From a515e228432dd5b876dddb9e836fa7e309d665dd Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Fri, 8 Mar 2024 15:32:26 -0500 Subject: [PATCH 01/34] Foundational block of ECS: type-erased `Vec`. See discussion at https://discord.com/channels/273534239310479360/1215712247654125588 --- inox2d/src/puppet.rs | 2 + inox2d/src/puppet/world.rs | 145 +++++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+) create mode 100644 inox2d/src/puppet/world.rs diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index af256fd..5cafd37 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -1,5 +1,7 @@ #![allow(dead_code)] +pub mod world; + use std::collections::HashMap; use std::fmt; diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs new file mode 100644 index 0000000..39a10de --- /dev/null +++ b/inox2d/src/puppet/world.rs @@ -0,0 +1,145 @@ +use std::any::TypeId; +use std::collections::HashMap; +use std::mem::{size_of, transmute, ManuallyDrop, MaybeUninit}; + +// TODO: complete readings on "provenance" +type VecBytes = [MaybeUninit; size_of::>()]; + +/// type erased vec +// so vec_bytes is aligned to the most +#[cfg_attr(target_pointer_width = "32", repr(align(4)))] +#[cfg_attr(target_pointer_width = "64", repr(align(8)))] +#[repr(C)] +struct AnyVec { + vec_bytes: VecBytes, + type_id: TypeId, + drop: fn(&mut VecBytes), +} + +impl Drop for AnyVec { + fn drop(&mut self) { + (self.drop)(&mut self.vec_bytes); + } +} + +impl AnyVec { + // Self is inherently Send + Sync as a pack of bytes regardless of inner type, which is bad + pub fn new() -> Self { + let vec = ManuallyDrop::new(Vec::::new()); + Self { + // SAFETY: ManuallyDrop guaranteed to have same bit layout as inner, and inner is a proper Vec + // note that "Vec::new() is allowed to return uninitialized bytes" + vec_bytes: unsafe { transmute(vec) }, + type_id: TypeId::of::(), + // SAFETY: only to be called once at end of lifetime, and vec_bytes contain a valid Vec throughout self lifetime + drop: |vec_bytes| unsafe { + let vec: Vec = transmute(*vec_bytes); + // be explicit :) + drop(vec); + }, + } + } + + /// T MUST be the same as in new::() for a same instance + pub unsafe fn downcast_unchecked(&self) -> &Vec { + transmute(&self.vec_bytes) + } + + /// T MUST be the same as in new::() for a same instance + pub unsafe fn downcast_mut_unchecked(&mut self) -> &mut Vec { + transmute(&mut self.vec_bytes) + } + + pub fn downcast(&self) -> Option<&Vec> { + if TypeId::of::() == self.type_id { + // SAFETY: T is the same as in new::() + Some(unsafe { self.downcast_unchecked() }) + } else { + None + } + } + + pub fn downcast_mut(&mut self) -> Option<&mut Vec> { + if TypeId::of::() == self.type_id { + // SAFETY: T is the same as in new::() + Some(unsafe { self.downcast_mut_unchecked() }) + } else { + None + } + } +} + +pub struct World { + columns: HashMap, +} + +#[cfg(test)] +mod tests { + mod any_vec { + use super::super::AnyVec; + + #[test] + fn new_and_drop_empty() { + let any_vec = AnyVec::new::<[u8; 1]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 2]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 3]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 4]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 5]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 6]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 7]>(); + drop(any_vec); + let any_vec = AnyVec::new::<[u8; 8]>(); + drop(any_vec); + } + + #[test] + fn push_and_get_and_set() { + #[derive(Debug, PartialEq, Eq)] + struct Data { + int: u32, + c: u8, + } + + let mut any_vec = AnyVec::new::(); + + unsafe { + any_vec.downcast_mut_unchecked().push(Data { int: 0, c: b'A' }); + any_vec.downcast_mut_unchecked().push(Data { int: 1, c: b'B' }); + any_vec.downcast_mut_unchecked().push(Data { int: 2, c: b'C' }); + + assert_eq!(any_vec.downcast_unchecked::()[0], Data { int: 0, c: b'A' }); + assert_eq!(any_vec.downcast_unchecked::()[1], Data { int: 1, c: b'B' }); + + any_vec.downcast_mut_unchecked::()[2].c = b'D'; + + assert_eq!(any_vec.downcast_unchecked::()[2], Data { int: 2, c: b'D' }); + } + } + + #[test] + fn safety() { + #[derive(Debug, PartialEq, Eq)] + struct Data { + int: u32, + } + struct OtherData {} + + let mut any_vec = AnyVec::new::(); + + assert!(any_vec.downcast::().is_some()); + assert!(any_vec.downcast_mut::().is_some()); + + any_vec.downcast_mut::().unwrap().push(Data { int: 1 }); + assert_eq!(any_vec.downcast::().unwrap()[0], Data { int: 1 }); + + assert!(any_vec.downcast::().is_none()); + assert!(any_vec.downcast_mut::().is_none()); + } + } +} From 5bef6636cc6aed2a51c7bf167b06378d16a58c28 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sat, 9 Mar 2024 09:57:33 -0500 Subject: [PATCH 02/34] Finished readings on provenance. https://www.ralfj.de/blog/2022/04/11/provenance-exposed.html --- inox2d/src/puppet/world.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs index 39a10de..8f63625 100644 --- a/inox2d/src/puppet/world.rs +++ b/inox2d/src/puppet/world.rs @@ -2,7 +2,8 @@ use std::any::TypeId; use std::collections::HashMap; use std::mem::{size_of, transmute, ManuallyDrop, MaybeUninit}; -// TODO: complete readings on "provenance" +// to keep the provenance of the pointer in Vec (or any data struct that contains pointers), +// after transmutation they should be hosted in such a container for the compiler to properly reason type VecBytes = [MaybeUninit; size_of::>()]; /// type erased vec @@ -28,7 +29,7 @@ impl AnyVec { let vec = ManuallyDrop::new(Vec::::new()); Self { // SAFETY: ManuallyDrop guaranteed to have same bit layout as inner, and inner is a proper Vec - // note that "Vec::new() is allowed to return uninitialized bytes" + // provenance considerations present, see comment for VecBytes vec_bytes: unsafe { transmute(vec) }, type_id: TypeId::of::(), // SAFETY: only to be called once at end of lifetime, and vec_bytes contain a valid Vec throughout self lifetime From d52f4ff9eff0fa442b111fe0808259e94e8b3689 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 10 Mar 2024 18:47:20 -0400 Subject: [PATCH 03/34] `World` for hosting all nodes of different comps of a puppet. Safe interfaces of `AnyVec` removed, as tested and usage constrained. --- inox2d/src/puppet/world.rs | 175 ++++++++++++++++++++++++++++++------- 1 file changed, 144 insertions(+), 31 deletions(-) diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs index 8f63625..c14bab3 100644 --- a/inox2d/src/puppet/world.rs +++ b/inox2d/src/puppet/world.rs @@ -2,18 +2,19 @@ use std::any::TypeId; use std::collections::HashMap; use std::mem::{size_of, transmute, ManuallyDrop, MaybeUninit}; +use super::InoxNodeUuid; + // to keep the provenance of the pointer in Vec (or any data struct that contains pointers), // after transmutation they should be hosted in such a container for the compiler to properly reason type VecBytes = [MaybeUninit; size_of::>()]; -/// type erased vec +/// type erased vec, only for World use. all methods unsafe as correctness solely dependent on usage // so vec_bytes is aligned to the most #[cfg_attr(target_pointer_width = "32", repr(align(4)))] #[cfg_attr(target_pointer_width = "64", repr(align(8)))] #[repr(C)] struct AnyVec { vec_bytes: VecBytes, - type_id: TypeId, drop: fn(&mut VecBytes), } @@ -31,7 +32,6 @@ impl AnyVec { // SAFETY: ManuallyDrop guaranteed to have same bit layout as inner, and inner is a proper Vec // provenance considerations present, see comment for VecBytes vec_bytes: unsafe { transmute(vec) }, - type_id: TypeId::of::(), // SAFETY: only to be called once at end of lifetime, and vec_bytes contain a valid Vec throughout self lifetime drop: |vec_bytes| unsafe { let vec: Vec = transmute(*vec_bytes); @@ -50,28 +50,98 @@ impl AnyVec { pub unsafe fn downcast_mut_unchecked(&mut self) -> &mut Vec { transmute(&mut self.vec_bytes) } +} + +pub struct World { + // Type -> (Column, Ownership) + columns: HashMap)>, +} + +pub trait Component: 'static + Send + Sync {} +impl Component for T {} - pub fn downcast(&self) -> Option<&Vec> { - if TypeId::of::() == self.type_id { - // SAFETY: T is the same as in new::() - Some(unsafe { self.downcast_unchecked() }) - } else { - None +impl World { + pub fn new() -> Self { + Self { + columns: HashMap::new(), } } - pub fn downcast_mut(&mut self) -> Option<&mut Vec> { - if TypeId::of::() == self.type_id { - // SAFETY: T is the same as in new::() - Some(unsafe { self.downcast_mut_unchecked() }) - } else { - None - } + /// adding a second component of the same type for a same node + /// - panics in debug + /// - silently discard the add in release + pub fn add(&mut self, node: InoxNodeUuid, v: T) { + let pair = self + .columns + .entry(TypeId::of::()) + .or_insert((AnyVec::new::(), HashMap::new())); + // SAFETY: AnyVec in pair must be of type T, enforced by hashing + let column = unsafe { pair.0.downcast_mut_unchecked() }; + + debug_assert!(!pair.1.contains_key(&node),); + pair.1.insert(node, column.len()); + column.push(v); + } + + pub fn get(&self, node: InoxNodeUuid) -> Option<&T> { + let pair = match self.columns.get(&TypeId::of::()) { + Some(c) => c, + None => return None, + }; + // SAFETY: AnyVec in pair must be of type T, enforced by hashing + let column = unsafe { pair.0.downcast_unchecked() }; + + let index = match pair.1.get(&node) { + Some(i) => *i, + None => return None, + }; + debug_assert!(index < column.len()); + // SAFETY: what has been inserted into pair.1 should be a valid index + Some(unsafe { column.get_unchecked(index) }) + } + + pub fn get_mut(&mut self, node: InoxNodeUuid) -> Option<&mut T> { + let pair = match self.columns.get_mut(&TypeId::of::()) { + Some(c) => c, + None => return None, + }; + // SAFETY: AnyVec in pair must be of type T, enforced by hashing + let column = unsafe { pair.0.downcast_mut_unchecked() }; + + let index = match pair.1.get(&node) { + Some(i) => *i, + None => return None, + }; + debug_assert!(index < column.len()); + // SAFETY: what has been inserted into pair.1 should be a valid index + Some(unsafe { column.get_unchecked_mut(index) }) + } + + /// # Safety + /// + /// call if node has added a comp of type T earlier + pub unsafe fn get_unchecked(&self, node: InoxNodeUuid) -> &T { + let pair = self.columns.get(&TypeId::of::()).unwrap_unchecked(); + let index = *pair.1.get(&node).unwrap_unchecked(); + + pair.0.downcast_unchecked().get_unchecked(index) + } + + /// # Safety + /// + /// call if node has added a comp of type T earlier + pub unsafe fn get_mut_unchecked(&mut self, node: InoxNodeUuid) -> &mut T { + let pair = self.columns.get_mut(&TypeId::of::()).unwrap_unchecked(); + let index = *pair.1.get(&node).unwrap_unchecked(); + + pair.0.downcast_mut_unchecked().get_unchecked_mut(index) } } -pub struct World { - columns: HashMap, +impl Default for World { + fn default() -> Self { + Self::new() + } } #[cfg(test)] @@ -122,25 +192,68 @@ mod tests { assert_eq!(any_vec.downcast_unchecked::()[2], Data { int: 2, c: b'D' }); } } + } + + mod world { + use super::super::{InoxNodeUuid, World}; + + struct CompA {} + struct CompB { + i: u32, + } + struct CompC { + f: f64, + } + + const NODE_0: InoxNodeUuid = InoxNodeUuid(2); + const NODE_1: InoxNodeUuid = InoxNodeUuid(3); + const NODE_2: InoxNodeUuid = InoxNodeUuid(5); #[test] - fn safety() { - #[derive(Debug, PartialEq, Eq)] - struct Data { - int: u32, - } - struct OtherData {} + fn safe_ops() { + let mut world = World::new(); - let mut any_vec = AnyVec::new::(); + assert!(world.get::(NODE_0).is_none()); - assert!(any_vec.downcast::().is_some()); - assert!(any_vec.downcast_mut::().is_some()); + world.add(NODE_0, CompA {}); + world.add(NODE_0, CompB { i: 114 }); + world.add(NODE_0, CompC { f: 5.14 }); + world.add(NODE_1, CompA {}); + world.add(NODE_2, CompA {}); + world.add(NODE_1, CompC { f: 19.19 }); + world.add(NODE_2, CompC { f: 8.10 }); - any_vec.downcast_mut::().unwrap().push(Data { int: 1 }); - assert_eq!(any_vec.downcast::().unwrap()[0], Data { int: 1 }); + assert!(world.get::(NODE_0).is_some()); + assert!(world.get::(NODE_1).is_none()); - assert!(any_vec.downcast::().is_none()); - assert!(any_vec.downcast_mut::().is_none()); + assert_eq!(world.get::(NODE_0).unwrap().i, 114); + assert_eq!(world.get::(NODE_2).unwrap().f, 8.10); + + world.get_mut::(NODE_2).unwrap().f = 8.93; + + assert_eq!(world.get::(NODE_2).unwrap().f, 8.93); + } + + #[test] + fn unsafe_ops() { + let mut world = World::default(); + + world.add(NODE_0, CompA {}); + world.add(NODE_0, CompB { i: 114 }); + world.add(NODE_0, CompC { f: 5.14 }); + world.add(NODE_1, CompA {}); + world.add(NODE_2, CompA {}); + world.add(NODE_1, CompC { f: 19.19 }); + world.add(NODE_2, CompC { f: 8.10 }); + + unsafe { + assert_eq!(world.get_unchecked::(NODE_0).i, 114); + assert_eq!(world.get_unchecked::(NODE_2).f, 8.10); + + world.get_mut_unchecked::(NODE_2).f = 8.93; + + assert_eq!(world.get_unchecked::(NODE_2).f, 8.93); + } } } } From 492c894a656058c450a48520487b89443243f60a Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Wed, 13 Mar 2024 14:48:42 -0400 Subject: [PATCH 04/34] Major ECS refactor. Inochi2D node types into Inox2D components, see comments in `node/components.rs`. `Puppet` as self-managed struct, reads INP file into a tree of nodes and a world of components to construct self. No `Copy/Clone` on `InoxNode`, components and `JsonValue`, cutting Aka.inp loading time to 1/3. --- inox2d/examples/parse-inp.rs | 17 +- inox2d/src/formats.rs | 9 + inox2d/src/formats/inp.rs | 5 +- inox2d/src/formats/json.rs | 13 +- inox2d/src/formats/payload.rs | 350 ++++++++++--------- inox2d/src/lib.rs | 3 - inox2d/src/mesh.rs | 242 ------------- inox2d/src/model.rs | 5 +- inox2d/src/node.rs | 39 +-- inox2d/src/node/components.rs | 18 + inox2d/src/node/components/composite.rs | 4 + inox2d/src/node/components/drawable.rs | 83 +++++ inox2d/src/node/components/simple_physics.rs | 50 +++ inox2d/src/node/components/textured_mesh.rs | 22 ++ inox2d/src/node/data.rs | 205 ----------- inox2d/src/node/tree.rs | 156 --------- inox2d/src/params.rs | 6 +- inox2d/src/puppet.rs | 297 ++-------------- inox2d/src/puppet/meta.rs | 212 +++++++++++ inox2d/src/puppet/tree.rs | 56 +++ inox2d/src/render.rs | 1 - 21 files changed, 691 insertions(+), 1102 deletions(-) delete mode 100644 inox2d/src/mesh.rs create mode 100644 inox2d/src/node/components.rs create mode 100644 inox2d/src/node/components/composite.rs create mode 100644 inox2d/src/node/components/drawable.rs create mode 100644 inox2d/src/node/components/simple_physics.rs create mode 100644 inox2d/src/node/components/textured_mesh.rs delete mode 100644 inox2d/src/node/data.rs delete mode 100644 inox2d/src/node/tree.rs create mode 100644 inox2d/src/puppet/meta.rs create mode 100644 inox2d/src/puppet/tree.rs diff --git a/inox2d/examples/parse-inp.rs b/inox2d/examples/parse-inp.rs index 1c1baf1..e1959b3 100644 --- a/inox2d/examples/parse-inp.rs +++ b/inox2d/examples/parse-inp.rs @@ -23,10 +23,23 @@ fn main() { data }; - let model = parse_inp(data.as_slice()).unwrap(); + use std::time::Instant; + let now = Instant::now(); + + let model = match parse_inp(data.as_slice()) { + Ok(m) => m, + Err(e) => { + println!("{e}"); + return; + } + }; + + let elapsed = now.elapsed(); + println!("parse_inp() took: {:.2?}", elapsed); println!("== Puppet Meta ==\n{}", &model.puppet.meta); - println!("== Nodes ==\n{}", &model.puppet.nodes); + // TODO: Implement full node print after ECS + // println!("== Nodes ==\n{}", &model.puppet.nodes); if model.vendors.is_empty() { println!("(No Vendor Data)\n"); } else { diff --git a/inox2d/src/formats.rs b/inox2d/src/formats.rs index da5b130..932e694 100644 --- a/inox2d/src/formats.rs +++ b/inox2d/src/formats.rs @@ -2,7 +2,10 @@ pub mod inp; mod json; mod payload; +use glam::Vec2; + use std::io::{self, Read}; +use std::slice; pub use json::JsonError; @@ -31,3 +34,9 @@ fn read_vec(data: &mut R, n: usize) -> io::Result> { data.read_exact(&mut buf)?; Ok(buf) } + +#[inline] +fn f32s_as_vec2s(vec: &[f32]) -> &'_ [Vec2] { + // SAFETY: the length of the slice never trespasses outside of the array + unsafe { slice::from_raw_parts(vec.as_ptr() as *const Vec2, vec.len() / 2) } +} diff --git a/inox2d/src/formats/inp.rs b/inox2d/src/formats/inp.rs index 4d3780c..05988f1 100644 --- a/inox2d/src/formats/inp.rs +++ b/inox2d/src/formats/inp.rs @@ -6,9 +6,10 @@ use std::sync::Arc; use image::ImageFormat; use crate::model::{Model, ModelTexture, VendorData}; +use crate::puppet::Puppet; use super::json::JsonError; -use super::payload::{deserialize_puppet, InoxParseError}; +use super::payload::InoxParseError; use super::{read_be_u32, read_n, read_u8, read_vec}; #[derive(Debug, thiserror::Error)] @@ -50,7 +51,7 @@ pub fn parse_inp(mut data: R) -> Result { let payload = read_vec(&mut data, length)?; let payload = std::str::from_utf8(&payload)?; let payload = json::parse(payload)?; - let puppet = deserialize_puppet(&payload)?; + let puppet = Puppet::new_from_json(&payload)?; // check texture section header let tex_sect = read_n::<_, 8>(&mut data).map_err(|_| ParseInpError::NoTexSect)?; diff --git a/inox2d/src/formats/json.rs b/inox2d/src/formats/json.rs index d7965e1..f6834e2 100644 --- a/inox2d/src/formats/json.rs +++ b/inox2d/src/formats/json.rs @@ -56,32 +56,33 @@ impl JsonError { } } +#[derive(Copy, Clone)] pub struct JsonObject<'a>(pub &'a json::object::Object); #[allow(unused)] impl<'a> JsonObject<'a> { - fn get(&self, key: &str) -> JsonResult<&json::JsonValue> { + fn get(self, key: &'a str) -> JsonResult<&json::JsonValue> { match self.0.get(key) { Some(value) => Ok(value), None => Err(JsonError::KeyDoesNotExist(key.to_owned())), } } - pub fn get_object(&self, key: &str) -> JsonResult { + pub fn get_object(self, key: &'a str) -> JsonResult { match self.get(key)?.as_object() { Some(obj) => Ok(JsonObject(obj)), None => Err(JsonError::ValueIsNotObject(key.to_owned())), } } - pub fn get_list(&self, key: &str) -> JsonResult<&[JsonValue]> { + pub fn get_list(self, key: &'a str) -> JsonResult<&[JsonValue]> { match self.get(key)? { json::JsonValue::Array(arr) => Ok(arr), _ => Err(JsonError::ValueIsNotList(key.to_owned())), } } - pub fn get_nullable_str(&self, key: &str) -> JsonResult> { + pub fn get_nullable_str(self, key: &'a str) -> JsonResult> { let val = self.get(key)?; if val.is_null() { return Ok(None); @@ -92,14 +93,14 @@ impl<'a> JsonObject<'a> { } } - pub fn get_str(&self, key: &str) -> JsonResult<&str> { + pub fn get_str(self, key: &'a str) -> JsonResult<&str> { match self.get(key)?.as_str() { Some(val) => Ok(val), None => Err(JsonError::ValueIsNotString(key.to_owned())), } } - fn get_number(&self, key: &str) -> JsonResult { + fn get_number(self, key: &'a str) -> JsonResult { match self.get(key)?.as_number() { Some(val) => Ok(val), None => Err(JsonError::ValueIsNotNumber(key.to_owned())), diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 163db9c..f398ed6 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -1,27 +1,18 @@ use std::collections::HashMap; use glam::{vec2, vec3, Vec2}; -use indextree::Arena; use json::JsonValue; use crate::math::interp::InterpolateMode; use crate::math::matrix::{Matrix2d, Matrix2dFromSliceVecsError}; use crate::math::transform::TransformOffset; -use crate::mesh::{f32s_as_vec2s, Mesh}; -use crate::node::data::{ - BlendMode, Composite, Drawable, InoxData, Mask, MaskMode, ParamMapMode, Part, PhysicsModel, PhysicsProps, - SimplePhysics, -}; -use crate::node::tree::InoxNodeTree; +use crate::node::components::{drawable::*, simple_physics::*, textured_mesh::*, *}; use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{AxisPoints, Binding, BindingValues, Param, ParamUuid}; -use crate::physics::runge_kutta::PhysicsState; -use crate::puppet::{ - Puppet, PuppetAllowedModification, PuppetAllowedRedistribution, PuppetAllowedUsers, PuppetMeta, PuppetPhysics, - PuppetUsageRights, -}; +use crate::puppet::{meta::*, Puppet, PuppetPhysics}; use crate::texture::TextureId; +use super::f32s_as_vec2s; use super::json::{JsonError, JsonObject, SerialExtend}; pub type InoxParseResult = Result; @@ -56,6 +47,8 @@ pub enum InoxParseError { Not2FloatsInList(usize), } +// json structure helpers + impl InoxParseError { pub fn nested(self, key: &str) -> Self { match self { @@ -76,41 +69,42 @@ fn as_nested_list(index: usize, val: &json::JsonValue) -> InoxParseResult<&[json } } -fn default_deserialize_custom(node_type: &str, _obj: &JsonObject) -> InoxParseResult { - Err(InoxParseError::UnknownNodeType(node_type.to_owned())) +fn as_object<'file>(msg: &str, val: &'file JsonValue) -> InoxParseResult> { + if let Some(obj) = val.as_object() { + Ok(JsonObject(obj)) + } else { + Err(InoxParseError::JsonError(JsonError::ValueIsNotObject(msg.to_owned()))) + } } -fn deserialize_node( - obj: &JsonObject, - deserialize_node_custom: &impl Fn(&str, &JsonObject) -> InoxParseResult, -) -> InoxParseResult> { - let node_type = obj.get_str("type")?; - Ok(InoxNode { - uuid: InoxNodeUuid(obj.get_u32("uuid")?), - name: obj.get_str("name")?.to_owned(), - enabled: obj.get_bool("enabled")?, - zsort: obj.get_f32("zsort")?, - trans_offset: vals("transform", deserialize_transform(&obj.get_object("transform")?))?, - lock_to_root: obj.get_bool("lockToRoot")?, - data: vals("data", deserialize_node_data(node_type, obj, deserialize_node_custom))?, - }) +// node deserialization + +struct ParsedNode<'file> { + node: InoxNode, + ty: &'file str, + data: JsonObject<'file>, + children: &'file [JsonValue], } -fn deserialize_node_data( - node_type: &str, - obj: &JsonObject, - deserialize_custom: &impl Fn(&str, &JsonObject) -> InoxParseResult, -) -> InoxParseResult> { - Ok(match node_type { - "Node" => InoxData::Node, - "Part" => InoxData::Part(deserialize_part(obj)?), - "Composite" => InoxData::Composite(deserialize_composite(obj)?), - "SimplePhysics" => InoxData::SimplePhysics(deserialize_simple_physics(obj)?), - node_type => InoxData::Custom((deserialize_custom)(node_type, obj)?), +fn deserialize_node(obj: JsonObject) -> InoxParseResult { + Ok(ParsedNode { + node: InoxNode { + uuid: InoxNodeUuid(obj.get_u32("uuid")?), + name: obj.get_str("name")?.to_owned(), + enabled: obj.get_bool("enabled")?, + zsort: obj.get_f32("zsort")?, + trans_offset: vals("transform", deserialize_transform(obj.get_object("transform")?))?, + lock_to_root: obj.get_bool("lockToRoot")?, + }, + ty: obj.get_str("type")?, + data: obj, + children: { obj.get_list("children").unwrap_or(&[]) }, }) } -fn deserialize_part(obj: &JsonObject) -> InoxParseResult { +// components deserialization + +fn deserialize_textured_mesh(obj: JsonObject) -> InoxParseResult { let (tex_albedo, tex_emissive, tex_bumpmap) = { let textures = obj.get_list("textures")?; @@ -143,27 +137,21 @@ fn deserialize_part(obj: &JsonObject) -> InoxParseResult { (tex_albedo, tex_emissive, tex_bumpmap) }; - Ok(Part { - draw_state: deserialize_drawable(obj)?, - mesh: vals("mesh", deserialize_mesh(&obj.get_object("mesh")?))?, + Ok(TexturedMesh { + mesh: vals("mesh", deserialize_mesh(obj.get_object("mesh")?))?, tex_albedo, tex_emissive, tex_bumpmap, }) } -fn deserialize_composite(obj: &JsonObject) -> InoxParseResult { - let draw_state = deserialize_drawable(obj)?; - Ok(Composite { draw_state }) -} - -fn deserialize_simple_physics(obj: &JsonObject) -> InoxParseResult { +fn deserialize_simple_physics(obj: JsonObject) -> InoxParseResult { Ok(SimplePhysics { param: ParamUuid(obj.get_u32("param")?), model_type: match obj.get_str("model_type")? { - "Pendulum" => PhysicsModel::RigidPendulum(PhysicsState::default()), - "SpringPendulum" => PhysicsModel::SpringPendulum(PhysicsState::default()), + "Pendulum" => PhysicsModel::RigidPendulum, + "SpringPendulum" => PhysicsModel::SpringPendulum, a => todo!("{}", a), }, map_mode: match obj.get_str("map_mode")? { @@ -182,44 +170,50 @@ fn deserialize_simple_physics(obj: &JsonObject) -> InoxParseResult InoxParseResult { +fn deserialize_drawable(obj: JsonObject) -> InoxParseResult { Ok(Drawable { - blend_mode: match obj.get_str("blend_mode")? { - "Normal" => BlendMode::Normal, - "Multiply" => BlendMode::Multiply, - "ColorDodge" => BlendMode::ColorDodge, - "LinearDodge" => BlendMode::LinearDodge, - "Screen" => BlendMode::Screen, - "ClipToLower" => BlendMode::ClipToLower, - "SliceFromLower" => BlendMode::SliceFromLower, - _ => BlendMode::default(), + blending: Blending { + mode: match obj.get_str("blend_mode")? { + "Normal" => BlendMode::Normal, + "Multiply" => BlendMode::Multiply, + "ColorDodge" => BlendMode::ColorDodge, + "LinearDodge" => BlendMode::LinearDodge, + "Screen" => BlendMode::Screen, + "ClipToLower" => BlendMode::ClipToLower, + "SliceFromLower" => BlendMode::SliceFromLower, + _ => BlendMode::default(), + }, + tint: obj.get_vec3("tint").unwrap_or(vec3(1.0, 1.0, 1.0)), + screen_tint: obj.get_vec3("screenTint").unwrap_or(vec3(0.0, 0.0, 0.0)), + opacity: obj.get_f32("opacity").unwrap_or(1.0), }, - tint: obj.get_vec3("tint").unwrap_or(vec3(1.0, 1.0, 1.0)), - screen_tint: obj.get_vec3("screenTint").unwrap_or(vec3(0.0, 0.0, 0.0)), - mask_threshold: obj.get_f32("mask_threshold").unwrap_or(0.5), masks: { if let Ok(masks) = obj.get_list("masks") { - masks - .iter() - .filter_map(|mask| deserialize_mask(&JsonObject(mask.as_object()?)).ok()) - .collect::>() + Some(Masks { + threshold: obj.get_f32("mask_threshold").unwrap_or(0.5), + masks: { + let mut collection = Vec::::new(); + for mask_obj in masks { + let mask = deserialize_mask(as_object("mask", mask_obj)?)?; + collection.push(mask); + } + collection + }, + }) } else { - Vec::new() + None } }, - opacity: obj.get_f32("opacity").unwrap_or(1.0), }) } -fn deserialize_mesh(obj: &JsonObject) -> InoxParseResult { +fn deserialize_mesh(obj: JsonObject) -> InoxParseResult { Ok(Mesh { - vertices: vals("verts", deserialize_vec2s_flat(obj.get_list("verts")?))?, - uvs: vals("uvs", deserialize_vec2s_flat(obj.get_list("uvs")?))?, + vertices: deserialize_vec2s_flat(obj.get_list("verts")?)?, + uvs: deserialize_vec2s_flat(obj.get_list("uvs")?)?, indices: obj .get_list("indices")? .iter() @@ -229,7 +223,7 @@ fn deserialize_mesh(obj: &JsonObject) -> InoxParseResult { }) } -fn deserialize_mask(obj: &JsonObject) -> InoxParseResult { +fn deserialize_mask(obj: JsonObject) -> InoxParseResult { Ok(Mask { source: InoxNodeUuid(obj.get_u32("source")?), mode: match obj.get_str("mode")? { @@ -240,7 +234,7 @@ fn deserialize_mask(obj: &JsonObject) -> InoxParseResult { }) } -fn deserialize_transform(obj: &JsonObject) -> InoxParseResult { +fn deserialize_transform(obj: JsonObject) -> InoxParseResult { Ok(TransformOffset { translation: obj.get_vec3("trans")?, rotation: obj.get_vec3("rot")?, @@ -285,42 +279,112 @@ fn deserialize_vec2s(vals: &[json::JsonValue]) -> InoxParseResult> { // Puppet deserialization -pub fn deserialize_puppet(val: &json::JsonValue) -> InoxParseResult { - deserialize_puppet_ext(val, &default_deserialize_custom) -} +impl Puppet { + pub fn new_from_json(payload: &json::JsonValue) -> InoxParseResult { + Self::new_from_json_with_custom(payload, None::<&fn(&mut Self, &str, JsonObject) -> InoxParseResult<()>>) + } -pub fn deserialize_puppet_ext( - val: &json::JsonValue, - deserialize_node_custom: &impl Fn(&str, &JsonObject) -> InoxParseResult, -) -> InoxParseResult> { - let Some(obj) = val.as_object() else { - return Err(InoxParseError::JsonError(JsonError::ValueIsNotObject( - "(puppet)".to_owned(), - ))); - }; - let obj = JsonObject(obj); + pub fn new_from_json_with_custom( + payload: &json::JsonValue, + load_node_data_custom: Option<&impl Fn(&mut Self, &str, JsonObject) -> InoxParseResult<()>>, + ) -> InoxParseResult { + let obj = as_object("(puppet)", payload)?; - let nodes = vals( - "nodes", - deserialize_nodes(&obj.get_object("nodes")?, deserialize_node_custom), - )?; + let meta = vals("meta", deserialize_puppet_meta(obj.get_object("meta")?))?; + let physics = vals("physics", deserialize_puppet_physics(obj.get_object("physics")?))?; + let parameters = deserialize_params(obj.get_list("param")?)?; - let meta = vals("meta", deserialize_puppet_meta(&obj.get_object("meta")?))?; + let root = vals("nodes", deserialize_node(obj.get_object("nodes")?))?; + let ParsedNode { + node, + ty, + data, + children, + } = root; + let root_id = node.uuid; - let physics = vals("physics", deserialize_puppet_physics(&obj.get_object("physics")?))?; + let mut puppet = Self::new(meta, physics, node, parameters); - let parameters = deserialize_params(obj.get_list("param")?); + puppet.load_node_data(root_id, ty, data, load_node_data_custom)?; + puppet.load_children_rec(root_id, children, load_node_data_custom)?; + + Ok(puppet) + } + + fn load_node_data( + &mut self, + id: InoxNodeUuid, + ty: &str, + data: JsonObject, + load_node_data_custom: Option<&impl Fn(&mut Self, &str, JsonObject) -> InoxParseResult<()>>, + ) -> InoxParseResult<()> { + match ty { + "Node" => (), + "Part" => { + self.node_comps.add(id, deserialize_drawable(data)?); + self.node_comps.add(id, deserialize_textured_mesh(data)?); + } + "Composite" => { + self.node_comps.add(id, deserialize_drawable(data)?); + self.node_comps.add(id, Composite {}); + } + "SimplePhysics" => { + self.node_comps.add(id, deserialize_simple_physics(data)?); + } + custom => { + if let Some(func) = load_node_data_custom { + func(self, custom, data)? + } + } + } + + Ok(()) + } - Ok(Puppet::new(meta, physics, nodes, parameters)) + fn load_children_rec( + &mut self, + id: InoxNodeUuid, + children: &[JsonValue], + load_node_data_custom: Option<&impl Fn(&mut Self, &str, JsonObject) -> InoxParseResult<()>>, + ) -> InoxParseResult<()> { + for (i, child) in children.iter().enumerate() { + let msg = &format!("children[{}]", i); + + let child = as_object("child", child).map_err(|e| e.nested(msg))?; + let child_node = deserialize_node(child).map_err(|e| e.nested(msg))?; + let ParsedNode { + node, + ty, + data, + children, + } = child_node; + let child_id = node.uuid; + + self.nodes.add(id, child_id, node); + self.load_node_data(child_id, ty, data, load_node_data_custom) + .map_err(|e| e.nested(msg))?; + if !children.is_empty() { + self.load_children_rec(child_id, children, load_node_data_custom) + .map_err(|e| e.nested(msg))?; + } + } + + Ok(()) + } } -fn deserialize_params(vals: &[json::JsonValue]) -> HashMap { - vals.iter() - .map_while(|param| deserialize_param(&JsonObject(param.as_object()?)).ok()) - .collect() +fn deserialize_params(vals: &[json::JsonValue]) -> InoxParseResult> { + let mut params = HashMap::new(); + + for param in vals { + let pair = deserialize_param(as_object("param", param)?)?; + params.insert(pair.0, pair.1); + } + + Ok(params) } -fn deserialize_param(obj: &JsonObject) -> InoxParseResult<(String, Param)> { +fn deserialize_param(obj: JsonObject) -> InoxParseResult<(String, Param)> { let name = obj.get_str("name")?.to_owned(); Ok(( name.clone(), @@ -331,19 +395,22 @@ fn deserialize_param(obj: &JsonObject) -> InoxParseResult<(String, Param)> { min: obj.get_vec2("min")?, max: obj.get_vec2("max")?, defaults: obj.get_vec2("defaults")?, - axis_points: vals("axis_points", deserialize_axis_points(obj.get_list("axis_points")?))?, - bindings: deserialize_bindings(obj.get_list("bindings")?), + axis_points: deserialize_axis_points(obj.get_list("axis_points")?)?, + bindings: deserialize_bindings(obj.get_list("bindings")?)?, }, )) } -fn deserialize_bindings(vals: &[json::JsonValue]) -> Vec { - vals.iter() - .filter_map(|binding| deserialize_binding(&JsonObject(binding.as_object()?)).ok()) - .collect() +fn deserialize_bindings(vals: &[json::JsonValue]) -> InoxParseResult> { + let mut bindings = Vec::new(); + for val in vals { + bindings.push(deserialize_binding(as_object("binding", val)?)?); + } + + Ok(bindings) } -fn deserialize_binding(obj: &JsonObject) -> InoxParseResult { +fn deserialize_binding(obj: JsonObject) -> InoxParseResult { let is_set = obj .get_list("isSet")? .iter() @@ -405,76 +472,21 @@ fn deserialize_axis_points(vals: &[json::JsonValue]) -> InoxParseResult( - obj: &JsonObject, - deserialize_node_custom: &impl Fn(&str, &JsonObject) -> InoxParseResult, -) -> InoxParseResult> { - let mut arena = Arena::new(); - let mut uuids = HashMap::new(); - - let root_node = deserialize_node(obj, deserialize_node_custom)?; - let root_uuid = root_node.uuid; - let root = arena.new_node(root_node); - uuids.insert(root_uuid, root); - - let mut node_tree = InoxNodeTree { root, arena, uuids }; - - for (i, child) in obj.get_list("children").unwrap_or(&[]).iter().enumerate() { - let Some(child) = child.as_object() else { - return Err(InoxParseError::JsonError(JsonError::ValueIsNotObject(format!( - "children[{i}]" - )))); - }; - - let child_id = deserialize_nodes_rec(&JsonObject(child), deserialize_node_custom, &mut node_tree) - .map_err(|e| e.nested(&format!("children[{i}]")))?; - - root.append(child_id, &mut node_tree.arena); - } - - Ok(node_tree) -} - -fn deserialize_nodes_rec( - obj: &JsonObject, - deserialize_node_custom: &impl Fn(&str, &JsonObject) -> InoxParseResult, - node_tree: &mut InoxNodeTree, -) -> InoxParseResult { - let node = deserialize_node(obj, deserialize_node_custom)?; - let uuid = node.uuid; - let node_id = node_tree.arena.new_node(node); - node_tree.uuids.insert(uuid, node_id); - - for (i, child) in obj.get_list("children").unwrap_or(&[]).iter().enumerate() { - let Some(child) = child.as_object() else { - return Err(InoxParseError::JsonError(JsonError::ValueIsNotObject(format!( - "children[{i}]" - )))); - }; - let child_id = deserialize_nodes_rec(&JsonObject(child), deserialize_node_custom, node_tree) - .map_err(|e| e.nested(&format!("children[{i}]")))?; - - node_id.append(child_id, &mut node_tree.arena); - } - - Ok(node_id) -} - -fn deserialize_puppet_physics(obj: &JsonObject) -> InoxParseResult { +fn deserialize_puppet_physics(obj: JsonObject) -> InoxParseResult { Ok(PuppetPhysics { pixels_per_meter: obj.get_f32("pixelsPerMeter")?, gravity: obj.get_f32("gravity")?, }) } -fn deserialize_puppet_meta(obj: &JsonObject) -> InoxParseResult { +fn deserialize_puppet_meta(obj: JsonObject) -> InoxParseResult { Ok(PuppetMeta { name: obj.get_nullable_str("name")?.map(str::to_owned), version: obj.get_str("version")?.to_owned(), rigger: obj.get_nullable_str("rigger")?.map(str::to_owned), artist: obj.get_nullable_str("artist")?.map(str::to_owned), rights: match obj.get_object("rights").ok() { - Some(ref rights) => Some(deserialize_puppet_usage_rights(rights)?), + Some(rights) => Some(deserialize_puppet_usage_rights(rights)?), None => None, }, copyright: obj.get_nullable_str("copyright")?.map(str::to_owned), @@ -486,7 +498,7 @@ fn deserialize_puppet_meta(obj: &JsonObject) -> InoxParseResult { }) } -fn deserialize_puppet_usage_rights(obj: &JsonObject) -> InoxParseResult { +fn deserialize_puppet_usage_rights(obj: JsonObject) -> InoxParseResult { Ok(PuppetUsageRights { allowed_users: match obj.get_str("allowed_users")? { "OnlyAuthor" => PuppetAllowedUsers::OnlyAuthor, diff --git a/inox2d/src/lib.rs b/inox2d/src/lib.rs index d2e21fe..3e171c8 100644 --- a/inox2d/src/lib.rs +++ b/inox2d/src/lib.rs @@ -1,12 +1,9 @@ pub mod formats; pub mod math; -pub mod mesh; pub mod model; pub mod node; pub mod params; -pub mod physics; pub mod puppet; -pub mod render; pub mod texture; pub const INOCHI2D_SPEC_VERSION: &str = "1.0-alpha"; diff --git a/inox2d/src/mesh.rs b/inox2d/src/mesh.rs deleted file mode 100644 index a50fa3b..0000000 --- a/inox2d/src/mesh.rs +++ /dev/null @@ -1,242 +0,0 @@ -use std::collections::BTreeMap; -use std::slice; - -use glam::{vec2, vec3, IVec2, Vec2, Vec4}; - -/// Mesh -#[derive(Clone, Debug, Default)] -pub struct Mesh { - /// Vertices in the mesh. - pub vertices: Vec, - /// Base UVs. - pub uvs: Vec, - /// Indices in the mesh. - pub indices: Vec, - /// Origin of the mesh. - pub origin: Vec2, -} - -impl Mesh { - /// Add a new vertex. - pub fn add(&mut self, vertex: Vec2, uv: Vec2) { - self.vertices.push(vertex); - self.uvs.push(uv); - } - - /// Clear connections/indices. - pub fn clear_connections(&mut self) { - self.indices.clear(); - } - - /// Connect 2 vertices together. - pub fn connect(&mut self, first: u16, second: u16) { - self.indices.extend([first, second]); - } - - /// Find the index of a vertex. - pub fn find(&self, vertex: Vec2) -> Option { - self.vertices.iter().position(|v| *v == vertex) - } - - /// Whether the mesh data is ready to be used. - pub fn is_ready(&self) -> bool { - self.can_triangulate() - } - - /// Whether the mesh data is ready to be triangulated. - pub fn can_triangulate(&self) -> bool { - !self.indices.is_empty() && self.indices.len() % 3 == 0 - } - - /// Fixes the winding order of a mesh. - #[allow(clippy::identity_op)] - pub fn fix_winding(&mut self) { - if !self.is_ready() { - return; - } - - for i in 0..self.indices.len() / 3 { - let i = i * 3; - - let vert_a: Vec2 = self.vertices[self.indices[i + 0] as usize]; - let vert_b: Vec2 = self.vertices[self.indices[i + 1] as usize]; - let vert_c: Vec2 = self.vertices[self.indices[i + 2] as usize]; - - let vert_ba = vert_b - vert_a; - let vert_ba = vec3(vert_ba.x, vert_ba.y, 0.); - let vert_ca = vert_c - vert_a; - let vert_ca = vec3(vert_ca.x, vert_ca.y, 0.); - - // Swap winding - if vert_ba.cross(vert_ca).z < 0. { - self.indices.swap(i + 1, i + 2); - } - } - } - - pub fn connections_at_point(&self, point: Vec2) -> usize { - self.find(point) - .map(|idx| self.connections_at_index(idx as u16)) - .unwrap_or(0) - } - - pub fn connections_at_index(&self, index: u16) -> usize { - self.indices.iter().filter(|&idx| *idx == index).count() - } - - pub fn vertices_as_f32s(&self) -> &'_ [f32] { - vec2s_as_f32s(&self.vertices) - } - - pub fn uvs_as_f32s(&self) -> &'_ [f32] { - vec2s_as_f32s(&self.uvs) - } - - /// Generates a quad-based mesh which is cut `cuts` amount of times. - /// - /// # Example - /// - /// ```ignore - /// Mesh::quad() - /// // Size of texture - /// .size(texture.width, texture.height) - /// // Uses all of UV - /// .uv_bounds(vec4(0., 0., 1., 1.)) - /// // width > height - /// .cuts(32, 16) - /// ``` - pub fn quad() -> QuadBuilder { - QuadBuilder::default() - } - - pub fn dbg_lens(&self) { - println!( - "lengths: v_{} u_{} i_{}", - self.vertices.len(), - self.uvs.len(), - self.indices.len() - ); - } -} - -pub(crate) fn vec2s_as_f32s(vec: &[Vec2]) -> &'_ [f32] { - // SAFETY: the length of the slice is always right - unsafe { slice::from_raw_parts(vec.as_ptr() as *const f32, vec.len() * 2) } -} - -pub(crate) fn f32s_as_vec2s(vec: &[f32]) -> &'_ [Vec2] { - // SAFETY: the length of the slice never trespasses outside of the array - unsafe { slice::from_raw_parts(vec.as_ptr() as *const Vec2, vec.len() / 2) } -} - -#[derive(Clone, Debug)] -pub struct QuadBuilder { - size: IVec2, - uv_bounds: Vec4, - cuts: IVec2, - origin: IVec2, -} - -impl Default for QuadBuilder { - fn default() -> Self { - Self { - size: Default::default(), - uv_bounds: Default::default(), - cuts: IVec2::new(6, 6), - origin: Default::default(), - } - } -} - -impl QuadBuilder { - /// Size of the mesh. - pub fn size(mut self, x: i32, y: i32) -> Self { - self.size = IVec2::new(x, y); - self - } - - /// x, y UV coordinates + width/height in UV coordinate space. - pub fn uv_bounds(mut self, uv_bounds: Vec4) -> Self { - self.uv_bounds = uv_bounds; - self - } - - /// Cuts are how many times to cut the mesh on the X and Y axis. - /// - /// Note: splits may not be below 2, so they are clamped automatically. - pub fn cuts(mut self, x: i32, y: i32) -> Self { - let x = x.max(2); - let y = y.max(2); - - self.cuts = IVec2::new(x, y); - self - } - - pub fn origin(mut self, x: i32, y: i32) -> Self { - self.origin = IVec2::new(x, y); - self - } - - pub fn build(self) -> Mesh { - let IVec2 { x: sw, y: sh } = self.size / self.cuts; - let uvx = self.uv_bounds.w / self.cuts.x as f32; - let uvy = self.uv_bounds.z / self.cuts.y as f32; - - let mut vert_map = BTreeMap::new(); - let mut vertices = Vec::new(); - let mut uvs = Vec::new(); - let mut indices = Vec::new(); - - // Generate vertices and UVs - for y in 0..=self.cuts.y { - for x in 0..=self.cuts.x { - vertices.push(vec2((x * sw - self.origin.x) as f32, (y * sh - self.origin.y) as f32)); - uvs.push(vec2( - self.uv_bounds.x + x as f32 * uvx, - self.uv_bounds.y + y as f32 * uvy, - )); - vert_map.insert((x, y), (vertices.len() - 1) as u16); - } - } - - // Generate indices - let center = self.cuts / 2; - for y in 0..center.y { - for x in 0..center.x { - // Indices - let idx0 = (x, y); - let idx1 = (x, y + 1); - let idx2 = (x + 1, y); - let idx3 = (x + 1, y + 1); - - // We want the vertices to generate in an X pattern so that we won't have too many distortion problems - if (x < center.x && y < center.y) || (x >= center.x && y >= center.y) { - indices.extend([ - vert_map[&idx0], - vert_map[&idx2], - vert_map[&idx3], - vert_map[&idx0], - vert_map[&idx3], - vert_map[&idx1], - ]); - } else { - indices.extend([ - vert_map[&idx0], - vert_map[&idx1], - vert_map[&idx2], - vert_map[&idx1], - vert_map[&idx2], - vert_map[&idx3], - ]); - } - } - } - - Mesh { - vertices, - uvs, - indices, - origin: Vec2::default(), - } - } -} diff --git a/inox2d/src/model.rs b/inox2d/src/model.rs index 9adefb7..37e308c 100644 --- a/inox2d/src/model.rs +++ b/inox2d/src/model.rs @@ -28,9 +28,8 @@ impl fmt::Display for VendorData { } /// Inochi2D model. -#[derive(Clone, Debug)] -pub struct Model { - pub puppet: Puppet, +pub struct Model { + pub puppet: Puppet, pub textures: Vec, pub vendors: Vec, } diff --git a/inox2d/src/node.rs b/inox2d/src/node.rs index 75f56f2..bcdee49 100644 --- a/inox2d/src/node.rs +++ b/inox2d/src/node.rs @@ -1,49 +1,16 @@ -pub mod data; -pub mod tree; - -use std::fmt::Debug; +pub mod components; use crate::math::transform::TransformOffset; -use data::InoxData; - -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[derive(Clone, Copy, Hash, Eq, PartialEq)] #[repr(transparent)] pub struct InoxNodeUuid(pub(crate) u32); -#[derive(Clone, Debug)] -pub struct InoxNode { +pub struct InoxNode { pub uuid: InoxNodeUuid, pub name: String, pub enabled: bool, pub zsort: f32, pub trans_offset: TransformOffset, pub lock_to_root: bool, - pub data: InoxData, -} - -impl InoxNode { - pub fn is_node(&self) -> bool { - self.data.is_node() - } - - pub fn is_part(&self) -> bool { - self.data.is_part() - } - - pub fn is_composite(&self) -> bool { - self.data.is_composite() - } - - pub fn is_simple_physics(&self) -> bool { - self.data.is_simple_physics() - } - - pub fn is_custom(&self) -> bool { - self.data.is_custom() - } - - pub fn node_type_name(&self) -> &'static str { - self.data.data_type_name() - } } diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs new file mode 100644 index 0000000..c4b1eec --- /dev/null +++ b/inox2d/src/node/components.rs @@ -0,0 +1,18 @@ +/*! +Inochi2D node types to Inox2D components: +- Node -> (Nothing) +- Part -> Drawable + TexturedMesh +- Composite -> Drawable + Composite +- SimplePhysics -> SimplePhysics +- Custom nodes by inheritance -> Custom nodes by composition +*/ + +pub mod composite; +pub mod drawable; +pub mod simple_physics; +pub mod textured_mesh; + +pub use composite::Composite; +pub use drawable::Drawable; +pub use simple_physics::SimplePhysics; +pub use textured_mesh::TexturedMesh; diff --git a/inox2d/src/node/components/composite.rs b/inox2d/src/node/components/composite.rs new file mode 100644 index 0000000..8eede65 --- /dev/null +++ b/inox2d/src/node/components/composite.rs @@ -0,0 +1,4 @@ +/// If has this as a component, the node should composite all children +/// +/// Empty as only a marker, zsorted children list constructed later on demand +pub struct Composite {} diff --git a/inox2d/src/node/components/drawable.rs b/inox2d/src/node/components/drawable.rs new file mode 100644 index 0000000..ffa5861 --- /dev/null +++ b/inox2d/src/node/components/drawable.rs @@ -0,0 +1,83 @@ +use glam::Vec3; + +use super::super::InoxNodeUuid; + +/// If has this as a component, the node should render something +pub struct Drawable { + pub blending: Blending, + /// If Some, the node should consider masking when rendering + pub masks: Option, +} + +pub struct Blending { + pub mode: BlendMode, + pub tint: Vec3, + pub screen_tint: Vec3, + pub opacity: f32, +} + +#[derive(Default)] +pub enum BlendMode { + /// Normal blending mode. + #[default] + Normal, + /// Multiply blending mode. + Multiply, + /// Color Dodge. + ColorDodge, + /// Linear Dodge. + LinearDodge, + /// Screen. + Screen, + /// Clip to Lower. + /// Special blending mode that clips the drawable + /// to a lower rendered area. + ClipToLower, + /// Slice from Lower. + /// Special blending mode that slices the drawable + /// via a lower rendered area. + /// (Basically inverse ClipToLower.) + SliceFromLower, +} + +impl BlendMode { + pub const VALUES: [BlendMode; 7] = [ + BlendMode::Normal, + BlendMode::Multiply, + BlendMode::ColorDodge, + BlendMode::LinearDodge, + BlendMode::Screen, + BlendMode::ClipToLower, + BlendMode::SliceFromLower, + ]; +} + +pub struct Masks { + pub threshold: f32, + pub masks: Vec, +} + +impl Masks { + /// Checks whether has masks of mode `MaskMode::Mask`. + pub fn has_masks(&self) -> bool { + self.masks.iter().any(|mask| mask.mode == MaskMode::Mask) + } + + /// Checks whether has masks of mode `MaskMode::Dodge`. + pub fn has_dodge_masks(&self) -> bool { + self.masks.iter().any(|mask| mask.mode == MaskMode::Dodge) + } +} + +pub struct Mask { + pub source: InoxNodeUuid, + pub mode: MaskMode, +} + +#[derive(PartialEq)] +pub enum MaskMode { + /// The part should be masked by the drawables specified. + Mask, + /// The path should be dodge-masked by the drawables specified. + Dodge, +} diff --git a/inox2d/src/node/components/simple_physics.rs b/inox2d/src/node/components/simple_physics.rs new file mode 100644 index 0000000..19337df --- /dev/null +++ b/inox2d/src/node/components/simple_physics.rs @@ -0,0 +1,50 @@ +use glam::Vec2; + +use crate::params::ParamUuid; + +/// If has this as a component, the node is capable of doing Inochi2D SimplePhysics simulations +pub struct SimplePhysics { + pub param: ParamUuid, + pub model_type: PhysicsModel, + pub map_mode: ParamMapMode, + pub props: PhysicsProps, + /// Whether physics system listens to local transform only. + pub local_only: bool, +} + +pub enum PhysicsModel { + RigidPendulum, + SpringPendulum, +} + +pub enum ParamMapMode { + AngleLength, + XY, +} + +pub struct PhysicsProps { + /// Gravity scale (1.0 = puppet gravity) + pub gravity: f32, + /// Pendulum/spring rest length (pixels) + pub length: f32, + /// Resonant frequency (Hz) + pub frequency: f32, + /// Angular damping ratio + pub angle_damping: f32, + /// Length damping ratio + pub length_damping: f32, + pub output_scale: Vec2, +} + +impl Default for PhysicsProps { + fn default() -> Self { + Self { + gravity: 1., + length: 1., + frequency: 1., + angle_damping: 0.5, + length_damping: 0.5, + output_scale: Vec2::ONE, + } + } +} diff --git a/inox2d/src/node/components/textured_mesh.rs b/inox2d/src/node/components/textured_mesh.rs new file mode 100644 index 0000000..fcadafb --- /dev/null +++ b/inox2d/src/node/components/textured_mesh.rs @@ -0,0 +1,22 @@ +use glam::Vec2; + +use crate::texture::TextureId; + +/// If has this as a component, the node should render a deformed texture +pub struct TexturedMesh { + pub mesh: Mesh, + pub tex_albedo: TextureId, + pub tex_emissive: TextureId, + pub tex_bumpmap: TextureId, +} + +pub struct Mesh { + /// Vertices in the mesh. + pub vertices: Vec, + /// Base UVs. + pub uvs: Vec, + /// Indices in the mesh. + pub indices: Vec, + /// Origin of the mesh. + pub origin: Vec2, +} diff --git a/inox2d/src/node/data.rs b/inox2d/src/node/data.rs deleted file mode 100644 index 805f68d..0000000 --- a/inox2d/src/node/data.rs +++ /dev/null @@ -1,205 +0,0 @@ -use glam::{Vec2, Vec3}; - -use crate::mesh::Mesh; -use crate::params::ParamUuid; -use crate::physics::pendulum::rigid::RigidPendulum; -use crate::physics::pendulum::spring::SpringPendulum; -use crate::physics::runge_kutta::PhysicsState; -use crate::texture::TextureId; - -use super::InoxNodeUuid; - -/// Blending mode. -#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub enum BlendMode { - /// Normal blending mode. - #[default] - Normal, - /// Multiply blending mode. - Multiply, - /// Color Dodge. - ColorDodge, - /// Linear Dodge. - LinearDodge, - /// Screen. - Screen, - /// Clip to Lower. - /// Special blending mode that clips the drawable - /// to a lower rendered area. - ClipToLower, - /// Slice from Lower. - /// Special blending mode that slices the drawable - /// via a lower rendered area. - /// (Basically inverse ClipToLower.) - SliceFromLower, -} - -impl BlendMode { - pub const VALUES: [BlendMode; 7] = [ - BlendMode::Normal, - BlendMode::Multiply, - BlendMode::ColorDodge, - BlendMode::LinearDodge, - BlendMode::Screen, - BlendMode::ClipToLower, - BlendMode::SliceFromLower, - ]; -} - -#[derive(Debug, Clone, Copy, PartialEq)] -pub enum MaskMode { - /// The part should be masked by the drawables specified. - Mask, - /// The path should be dodge-masked by the drawables specified. - Dodge, -} - -#[derive(Debug, Clone, PartialEq)] -pub struct Mask { - pub source: InoxNodeUuid, - pub mode: MaskMode, -} - -#[derive(Debug, Clone)] -pub struct Drawable { - pub blend_mode: BlendMode, - pub tint: Vec3, - pub screen_tint: Vec3, - pub mask_threshold: f32, - pub masks: Vec, - pub opacity: f32, -} - -impl Drawable { - /// Checks whether the drawable has masks of mode `MaskMode::Mask`. - pub fn has_masks(&self) -> bool { - self.masks.iter().any(|mask| mask.mode == MaskMode::Mask) - } - - /// Checks whether the drawable has masks of mode `MaskMode::Dodge`. - pub fn has_dodge_masks(&self) -> bool { - self.masks.iter().any(|mask| mask.mode == MaskMode::Dodge) - } -} - -#[derive(Debug, Clone)] -pub struct Part { - pub draw_state: Drawable, - pub mesh: Mesh, - pub tex_albedo: TextureId, - pub tex_emissive: TextureId, - pub tex_bumpmap: TextureId, -} - -#[derive(Debug, Clone)] -pub struct Composite { - pub draw_state: Drawable, -} - -// TODO: PhysicsModel should just be a flat enum with no physics state. -// There's no reason to store a state if we're not simulating anything. -// This can be fixed in the component refactor. -// (I didn't want to create yet another separate PhysicsCtx just for this.) - -/// Physics model to use for simple physics -#[derive(Debug, Clone)] -pub enum PhysicsModel { - /// Rigid pendulum - RigidPendulum(PhysicsState), - - /// Springy pendulum - SpringPendulum(PhysicsState), -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] -pub enum ParamMapMode { - AngleLength, - XY, -} - -#[derive(Debug, Clone)] -pub struct PhysicsProps { - /// Gravity scale (1.0 = puppet gravity) - pub gravity: f32, - /// Pendulum/spring rest length (pixels) - pub length: f32, - /// Resonant frequency (Hz) - pub frequency: f32, - /// Angular damping ratio - pub angle_damping: f32, - /// Length damping ratio - pub length_damping: f32, - - pub output_scale: Vec2, -} - -impl Default for PhysicsProps { - fn default() -> Self { - Self { - gravity: 1., - length: 1., - frequency: 1., - angle_damping: 0.5, - length_damping: 0.5, - output_scale: Vec2::ONE, - } - } -} - -#[derive(Debug, Clone)] -pub struct SimplePhysics { - pub param: ParamUuid, - - pub model_type: PhysicsModel, - pub map_mode: ParamMapMode, - - pub props: PhysicsProps, - - /// Whether physics system listens to local transform only. - pub local_only: bool, - - // TODO: same as above, this state shouldn't be here. - // It is only useful when simulating physics. - pub bob: Vec2, -} - -#[derive(Debug, Clone)] -pub enum InoxData { - Node, - Part(Part), - Composite(Composite), - SimplePhysics(SimplePhysics), - Custom(T), -} - -impl InoxData { - pub fn is_node(&self) -> bool { - matches!(self, InoxData::Node) - } - - pub fn is_part(&self) -> bool { - matches!(self, InoxData::Part(_)) - } - - pub fn is_composite(&self) -> bool { - matches!(self, InoxData::Composite(_)) - } - - pub fn is_simple_physics(&self) -> bool { - matches!(self, InoxData::SimplePhysics(_)) - } - - pub fn is_custom(&self) -> bool { - matches!(self, InoxData::Custom(_)) - } - - pub fn data_type_name(&self) -> &'static str { - match self { - InoxData::Node => "Node", - InoxData::Part(_) => "Part", - InoxData::Composite(_) => "Composite", - InoxData::SimplePhysics(_) => "SimplePhysics", - InoxData::Custom(_) => "Custom", - } - } -} diff --git a/inox2d/src/node/tree.rs b/inox2d/src/node/tree.rs deleted file mode 100644 index 9176b7c..0000000 --- a/inox2d/src/node/tree.rs +++ /dev/null @@ -1,156 +0,0 @@ -use std::collections::HashMap; -use std::fmt::Display; - -use indextree::{Arena, NodeId}; - -use super::{InoxNode, InoxNodeUuid}; - -#[derive(Clone, Debug)] -pub struct InoxNodeTree { - pub root: indextree::NodeId, - pub arena: Arena>, - pub uuids: HashMap, -} - -impl InoxNodeTree { - fn get_internal_node(&self, uuid: InoxNodeUuid) -> Option<&indextree::Node>> { - self.arena.get(*self.uuids.get(&uuid)?) - } - fn get_internal_node_mut(&mut self, uuid: InoxNodeUuid) -> Option<&mut indextree::Node>> { - self.arena.get_mut(*self.uuids.get(&uuid)?) - } - - #[allow(clippy::borrowed_box)] - pub fn get_node(&self, uuid: InoxNodeUuid) -> Option<&InoxNode> { - Some(self.get_internal_node(uuid)?.get()) - } - - pub fn get_node_mut(&mut self, uuid: InoxNodeUuid) -> Option<&mut InoxNode> { - Some(self.get_internal_node_mut(uuid)?.get_mut()) - } - - #[allow(clippy::borrowed_box)] - pub fn get_parent(&self, uuid: InoxNodeUuid) -> Option<&InoxNode> { - let node = self.get_internal_node(uuid)?; - Some(self.arena.get(node.parent()?)?.get()) - } - - pub fn children_uuids(&self, uuid: InoxNodeUuid) -> Option> { - let node = self.get_internal_node(uuid)?; - let node_id = self.arena.get_node_id(node)?; - Some( - node_id - .children(&self.arena) - .filter_map(|nid| self.arena.get(nid)) - .map(|nod| nod.get().uuid) - .collect::>(), - ) - } - - fn rec_all_childen_from_node( - &self, - node: &InoxNode, - zsort: f32, - skip_composites: bool, - ) -> Vec<(InoxNodeUuid, f32)> { - let node_state = node; - let zsort = zsort + node_state.zsort; - let mut vec = vec![(node_state.uuid, zsort)]; - - // Skip composite children because they're a special case - if !skip_composites || !node.data.is_composite() { - for child_uuid in self.children_uuids(node.uuid).unwrap_or_default() { - if let Some(child) = self.get_node(child_uuid) { - vec.extend(self.rec_all_childen_from_node(child, zsort, skip_composites)); - } - } - } - - vec - } - - pub fn ancestors(&self, uuid: InoxNodeUuid) -> indextree::Ancestors> { - self.uuids[&uuid].ancestors(&self.arena) - } - - fn sort_by_zsort(&self, node: &InoxNode, skip_composites: bool) -> Vec { - let uuid_zsorts = self.rec_all_childen_from_node(node, 0.0, skip_composites); - sort_uuids_by_zsort(uuid_zsorts) - } - - /// all nodes, zsorted, with composite children excluded - pub fn zsorted_root(&self) -> Vec { - let root = self.arena.get(self.root).unwrap().get(); - self.sort_by_zsort(root, true) - } - - /// all children, grandchildren..., zsorted, with parent excluded - pub fn zsorted_children(&self, id: InoxNodeUuid) -> Vec { - let node = self.arena.get(self.uuids[&id]).unwrap().get(); - self.sort_by_zsort(node, false) - .into_iter() - .filter(|uuid| *uuid != node.uuid) - .collect::>() - } - - /// all nodes - pub fn all_node_ids(&self) -> Vec { - self.arena.iter().map(|n| n.get().uuid).collect() - } -} - -fn rec_fmt( - indent: usize, - f: &mut std::fmt::Formatter<'_>, - node_id: NodeId, - arena: &Arena>, -) -> std::fmt::Result { - let Some(node) = arena.get(node_id) else { - return Ok(()); - }; - - let node = node.get(); - - let type_name = node.node_type_name(); - #[cfg(feature = "owo")] - let type_name = { - use owo_colors::OwoColorize; - type_name.magenta() - }; - - writeln!(f, "{}- [{}] {}", " ".repeat(indent), type_name, node.name)?; - for child in node_id.children(arena) { - rec_fmt(indent + 1, f, child, arena)?; - } - - Ok(()) -} - -impl Display for InoxNodeTree { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - let Some(root_node) = self.arena.get(self.root) else { - return write!(f, "(empty)"); - }; - - let root_node = root_node.get(); - - let type_name = root_node.node_type_name(); - #[cfg(feature = "owo")] - let type_name = { - use owo_colors::OwoColorize; - type_name.magenta() - }; - - writeln!(f, "- [{}] {}", type_name, root_node.name)?; - for child in self.root.children(&self.arena) { - rec_fmt(1, f, child, &self.arena)?; - } - - Ok(()) - } -} - -fn sort_uuids_by_zsort(mut uuid_zsorts: Vec<(InoxNodeUuid, f32)>) -> Vec { - uuid_zsorts.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap().reverse()); - uuid_zsorts.into_iter().map(|(uuid, _zsort)| uuid).collect() -} diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 71d471f..604e849 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -4,10 +4,9 @@ use crate::math::interp::{bi_interpolate_f32, bi_interpolate_vec2s_additive, Int use crate::math::matrix::Matrix2d; use crate::node::InoxNodeUuid; use crate::puppet::Puppet; -use crate::render::{NodeRenderCtxs, PartRenderCtx, RenderCtxKind}; +// use crate::render::{NodeRenderCtxs, PartRenderCtx, RenderCtxKind}; /// Parameter binding to a node. This allows to animate a node based on the value of the parameter that owns it. -#[derive(Debug, Clone)] pub struct Binding { pub node: InoxNodeUuid, pub is_set: Matrix2d, @@ -50,7 +49,6 @@ fn ranges_out( pub struct ParamUuid(pub u32); /// Parameter. A simple bounded value that is used to animate nodes through bindings. -#[derive(Debug, Clone)] pub struct Param { pub uuid: ParamUuid, pub name: String, @@ -62,6 +60,7 @@ pub struct Param { pub bindings: Vec, } +/* impl Param { pub fn apply(&self, val: Vec2, node_render_ctxs: &mut NodeRenderCtxs, deform_buf: &mut [Vec2]) { let val = val.clamp(self.min, self.max); @@ -243,3 +242,4 @@ impl Puppet { Ok(()) } } +*/ diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 5cafd37..ccc32f5 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -1,287 +1,30 @@ -#![allow(dead_code)] - +pub mod meta; +pub mod tree; pub mod world; use std::collections::HashMap; -use std::fmt; -use crate::node::data::InoxData; -use crate::node::tree::InoxNodeTree; -use crate::node::InoxNodeUuid; +use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{Param, ParamUuid}; -use crate::render::RenderCtx; - -/// Who is allowed to use the puppet? -#[derive(Clone, Copy, Debug, Default)] -pub enum PuppetAllowedUsers { - /// Only the author(s) are allowed to use the puppet. - #[default] - OnlyAuthor, - /// Only licensee(s) are allowed to use the puppet. - OnlyLicensee, - /// Everyone may use the model. - Everyone, -} - -impl fmt::Display for PuppetAllowedUsers { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - PuppetAllowedUsers::OnlyAuthor => "only author", - PuppetAllowedUsers::OnlyLicensee => "only licensee", - PuppetAllowedUsers::Everyone => "Everyone", - } - ) - } -} - -/// Can the puppet be redistributed? -#[derive(Clone, Copy, Debug, Default)] -pub enum PuppetAllowedRedistribution { - /// Redistribution is prohibited - #[default] - Prohibited, - /// Redistribution is allowed, but only under the same license - /// as the original. - ViralLicense, - /// Redistribution is allowed, and the puppet may be - /// redistributed under a different license than the original. - /// - /// This goes in conjunction with modification rights. - CopyleftLicense, -} -impl fmt::Display for PuppetAllowedRedistribution { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - PuppetAllowedRedistribution::Prohibited => "prohibited", - PuppetAllowedRedistribution::ViralLicense => "viral license", - PuppetAllowedRedistribution::CopyleftLicense => "copyleft license", - } - ) - } -} - -/// Can the puppet be modified? -#[derive(Clone, Copy, Debug, Default)] -pub enum PuppetAllowedModification { - /// Modification is prohibited - #[default] - Prohibited, - /// Modification is only allowed for personal use - AllowPersonal, - /// Modification is allowed with redistribution, see - /// `allowed_redistribution` for redistribution terms. - AllowRedistribute, -} - -impl fmt::Display for PuppetAllowedModification { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - write!( - f, - "{}", - match self { - PuppetAllowedModification::Prohibited => "prohibited", - PuppetAllowedModification::AllowPersonal => "allow personal", - PuppetAllowedModification::AllowRedistribute => "allow redistribute", - } - ) - } -} - -/// Terms of usage of the puppet. -#[derive(Clone, Debug, Default)] -pub struct PuppetUsageRights { - /// Who is allowed to use the puppet? - pub allowed_users: PuppetAllowedUsers, - /// Whether violence content is allowed. - pub allow_violence: bool, - /// Whether sexual content is allowed. - pub allow_sexual: bool, - /// Whether commercial use is allowed. - pub allow_commercial: bool, - /// Whether a model may be redistributed. - pub allow_redistribution: PuppetAllowedRedistribution, - /// Whether a model may be modified. - pub allow_modification: PuppetAllowedModification, - /// Whether the author(s) must be attributed for use. - pub require_attribution: bool, -} - -fn allowed_bool(value: bool) -> &'static str { - if value { - "allowed" - } else { - "prohibited" - } -} - -impl fmt::Display for PuppetUsageRights { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { - writeln!(f, "| allowed users: {}", self.allowed_users)?; - writeln!(f, "| violence: {}", allowed_bool(self.allow_violence))?; - writeln!(f, "| sexual: {}", allowed_bool(self.allow_sexual))?; - writeln!(f, "| commercial: {}", allowed_bool(self.allow_commercial))?; - writeln!(f, "| redistribution: {}", self.allow_redistribution)?; - writeln!(f, "| modification: {}", self.allow_modification)?; - writeln!( - f, - "| attribution: {}", - if self.require_attribution { - "required" - } else { - "not required" - } - ) - } -} - -/// Puppet meta information. -#[derive(Clone, Debug)] -pub struct PuppetMeta { - /// Name of the puppet. - pub name: Option, - /// Version of the Inochi2D spec that was used when creating this model. - pub version: String, - /// Rigger(s) of the puppet. - pub rigger: Option, - /// Artist(s) of the puppet. - pub artist: Option, - /// Usage Rights of the puppet. - pub rights: Option, - /// Copyright string. - pub copyright: Option, - /// URL of the license. - pub license_url: Option, - /// Contact information of the first author. - pub contact: Option, - /// Link to the origin of this puppet. - pub reference: Option, - /// Texture ID of this puppet's thumbnail. - pub thumbnail_id: Option, - /// Whether the puppet should preserve pixel borders. - /// This feature is mainly useful for puppets that use pixel art. - pub preserve_pixels: bool, -} - -fn writeln_opt(f: &mut fmt::Formatter<'_>, field_name: &str, opt: &Option) -> fmt::Result { - let field_name = format!("{:<17}", format!("{field_name}:")); - if let Some(ref value) = opt { - #[cfg(feature = "owo")] - let value = { - use owo_colors::OwoColorize; - value.green() - }; - writeln!(f, "{field_name}{value}")?; - } - Ok(()) -} - -impl fmt::Display for PuppetMeta { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.name { - Some(ref name) => writeln_opt(f, "Name", &Some(name))?, - None => { - let no_name = "(No Name)"; - #[cfg(feature = "owo")] - let no_name = { - use owo_colors::OwoColorize; - no_name.dimmed() - }; - writeln!(f, "{no_name}")? - } - } - - writeln_opt(f, "Version", &Some(&self.version))?; - writeln_opt(f, "Rigger", &self.rigger)?; - writeln_opt(f, "Artist", &self.artist)?; - - if let Some(ref rights) = self.rights { - writeln!(f, "Rights:")?; - #[cfg(feature = "owo")] - let rights = { - use owo_colors::OwoColorize; - rights.yellow() - }; - writeln!(f, "{rights}")?; - } - - writeln_opt(f, "Copyright", &self.copyright)?; - writeln_opt(f, "License URL", &self.license_url)?; - writeln_opt(f, "Contact", &self.contact)?; - writeln_opt(f, "Reference", &self.reference)?; - writeln_opt(f, "Thumbnail ID", &self.thumbnail_id)?; - - writeln_opt( - f, - "Preserve pixels", - &Some(if self.preserve_pixels { "yes" } else { "no" }), - ) - } -} - -impl Default for PuppetMeta { - fn default() -> Self { - Self { - name: Default::default(), - version: crate::INOCHI2D_SPEC_VERSION.to_owned(), - rigger: Default::default(), - artist: Default::default(), - rights: Default::default(), - copyright: Default::default(), - license_url: Default::default(), - contact: Default::default(), - reference: Default::default(), - thumbnail_id: Default::default(), - preserve_pixels: Default::default(), - } - } -} - -/// Global physics parameters for the puppet. -#[derive(Clone, Copy, Debug)] -pub struct PuppetPhysics { - pub pixels_per_meter: f32, - pub gravity: f32, -} +use meta::PuppetMeta; +use tree::InoxNodeTree; +use world::World; /// Inochi2D puppet. -#[derive(Clone, Debug)] -pub struct Puppet { +pub struct Puppet { pub meta: PuppetMeta, - pub physics: PuppetPhysics, - pub nodes: InoxNodeTree, - pub drivers: Vec, + physics: PuppetPhysics, + // TODO: define the actual ctx + physics_ctx: Option>>, + pub nodes: InoxNodeTree, + pub node_comps: World, pub(crate) params: HashMap, pub(crate) param_names: HashMap, - pub render_ctx: RenderCtx, } -impl Puppet { - pub fn new( - meta: PuppetMeta, - physics: PuppetPhysics, - nodes: InoxNodeTree, - named_params: HashMap, - ) -> Self { - let render_ctx = RenderCtx::new(&nodes); - - let drivers = (nodes.arena.iter()) - .filter_map(|node| { - let node = node.get(); - - match node.data { - InoxData::SimplePhysics(_) => Some(node.uuid), - _ => None, - } - }) - .collect::>(); - +impl Puppet { + pub fn new(meta: PuppetMeta, physics: PuppetPhysics, root: InoxNode, named_params: HashMap) -> Self { let mut params = HashMap::new(); let mut param_names = HashMap::new(); for (name, param) in named_params { @@ -292,11 +35,17 @@ impl Puppet { Self { meta, physics, - nodes, - drivers, + physics_ctx: None, + nodes: InoxNodeTree::new_with_root(root), + node_comps: World::new(), params, param_names, - render_ctx, } } } + +/// Global physics parameters for the puppet. +pub struct PuppetPhysics { + pub pixels_per_meter: f32, + pub gravity: f32, +} diff --git a/inox2d/src/puppet/meta.rs b/inox2d/src/puppet/meta.rs new file mode 100644 index 0000000..dd470c9 --- /dev/null +++ b/inox2d/src/puppet/meta.rs @@ -0,0 +1,212 @@ +use std::fmt; + +pub struct PuppetMeta { + /// Name of the puppet. + pub name: Option, + /// Version of the Inochi2D spec that was used when creating this model. + pub version: String, + /// Rigger(s) of the puppet. + pub rigger: Option, + /// Artist(s) of the puppet. + pub artist: Option, + /// Usage Rights of the puppet. + pub rights: Option, + /// Copyright string. + pub copyright: Option, + /// URL of the license. + pub license_url: Option, + /// Contact information of the first author. + pub contact: Option, + /// Link to the origin of this puppet. + pub reference: Option, + /// Texture ID of this puppet's thumbnail. + pub thumbnail_id: Option, + /// Whether the puppet should preserve pixel borders. + /// This feature is mainly useful for puppets that use pixel art. + pub preserve_pixels: bool, +} + +fn writeln_opt(f: &mut fmt::Formatter<'_>, field_name: &str, opt: &Option) -> fmt::Result { + let field_name = format!("{:<17}", format!("{field_name}:")); + if let Some(ref value) = opt { + #[cfg(feature = "owo")] + let value = { + use owo_colors::OwoColorize; + value.green() + }; + writeln!(f, "{field_name}{value}")?; + } + Ok(()) +} + +impl fmt::Display for PuppetMeta { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.name { + Some(ref name) => writeln_opt(f, "Name", &Some(name))?, + None => { + let no_name = "(No Name)"; + #[cfg(feature = "owo")] + let no_name = { + use owo_colors::OwoColorize; + no_name.dimmed() + }; + writeln!(f, "{no_name}")? + } + } + + writeln_opt(f, "Version", &Some(&self.version))?; + writeln_opt(f, "Rigger", &self.rigger)?; + writeln_opt(f, "Artist", &self.artist)?; + + if let Some(ref rights) = self.rights { + writeln!(f, "Rights:")?; + #[cfg(feature = "owo")] + let rights = { + use owo_colors::OwoColorize; + rights.yellow() + }; + writeln!(f, "{rights}")?; + } + + writeln_opt(f, "Copyright", &self.copyright)?; + writeln_opt(f, "License URL", &self.license_url)?; + writeln_opt(f, "Contact", &self.contact)?; + writeln_opt(f, "Reference", &self.reference)?; + writeln_opt(f, "Thumbnail ID", &self.thumbnail_id)?; + + writeln_opt( + f, + "Preserve pixels", + &Some(if self.preserve_pixels { "yes" } else { "no" }), + ) + } +} + +/// Terms of usage of the puppet. +pub struct PuppetUsageRights { + /// Who is allowed to use the puppet? + pub allowed_users: PuppetAllowedUsers, + /// Whether violence content is allowed. + pub allow_violence: bool, + /// Whether sexual content is allowed. + pub allow_sexual: bool, + /// Whether commercial use is allowed. + pub allow_commercial: bool, + /// Whether a model may be redistributed. + pub allow_redistribution: PuppetAllowedRedistribution, + /// Whether a model may be modified. + pub allow_modification: PuppetAllowedModification, + /// Whether the author(s) must be attributed for use. + pub require_attribution: bool, +} + +fn allowed_bool(value: bool) -> &'static str { + if value { + "allowed" + } else { + "prohibited" + } +} + +impl fmt::Display for PuppetUsageRights { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + writeln!(f, "| allowed users: {}", self.allowed_users)?; + writeln!(f, "| violence: {}", allowed_bool(self.allow_violence))?; + writeln!(f, "| sexual: {}", allowed_bool(self.allow_sexual))?; + writeln!(f, "| commercial: {}", allowed_bool(self.allow_commercial))?; + writeln!(f, "| redistribution: {}", self.allow_redistribution)?; + writeln!(f, "| modification: {}", self.allow_modification)?; + writeln!( + f, + "| attribution: {}", + if self.require_attribution { + "required" + } else { + "not required" + } + ) + } +} + +/// Who is allowed to use the puppet? +#[derive(Default)] +pub enum PuppetAllowedUsers { + /// Only the author(s) are allowed to use the puppet. + #[default] + OnlyAuthor, + /// Only licensee(s) are allowed to use the puppet. + OnlyLicensee, + /// Everyone may use the model. + Everyone, +} + +impl fmt::Display for PuppetAllowedUsers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + PuppetAllowedUsers::OnlyAuthor => "only author", + PuppetAllowedUsers::OnlyLicensee => "only licensee", + PuppetAllowedUsers::Everyone => "Everyone", + } + ) + } +} + +/// Can the puppet be redistributed? +#[derive(Default)] +pub enum PuppetAllowedRedistribution { + /// Redistribution is prohibited + #[default] + Prohibited, + /// Redistribution is allowed, but only under the same license + /// as the original. + ViralLicense, + /// Redistribution is allowed, and the puppet may be + /// redistributed under a different license than the original. + /// + /// This goes in conjunction with modification rights. + CopyleftLicense, +} + +impl fmt::Display for PuppetAllowedRedistribution { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + PuppetAllowedRedistribution::Prohibited => "prohibited", + PuppetAllowedRedistribution::ViralLicense => "viral license", + PuppetAllowedRedistribution::CopyleftLicense => "copyleft license", + } + ) + } +} + +/// Can the puppet be modified? +#[derive(Default)] +pub enum PuppetAllowedModification { + /// Modification is prohibited + #[default] + Prohibited, + /// Modification is only allowed for personal use + AllowPersonal, + /// Modification is allowed with redistribution, see + /// `allowed_redistribution` for redistribution terms. + AllowRedistribute, +} + +impl fmt::Display for PuppetAllowedModification { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + PuppetAllowedModification::Prohibited => "prohibited", + PuppetAllowedModification::AllowPersonal => "allow personal", + PuppetAllowedModification::AllowRedistribute => "allow redistribute", + } + ) + } +} diff --git a/inox2d/src/puppet/tree.rs b/inox2d/src/puppet/tree.rs new file mode 100644 index 0000000..ca63afa --- /dev/null +++ b/inox2d/src/puppet/tree.rs @@ -0,0 +1,56 @@ +use std::collections::HashMap; + +use indextree::Arena; + +use crate::node::{InoxNode, InoxNodeUuid}; + +pub struct InoxNodeTree { + root_id: indextree::NodeId, + arena: Arena, + node_ids: HashMap, +} + +impl InoxNodeTree { + pub fn new_with_root(node: InoxNode) -> Self { + let id = node.uuid; + let mut node_ids = HashMap::new(); + let mut arena = Arena::new(); + + let root_id = arena.new_node(node); + node_ids.insert(id, root_id); + + Self { + root_id, + arena, + node_ids, + } + } + + pub fn add(&mut self, parent: InoxNodeUuid, id: InoxNodeUuid, node: InoxNode) { + let parent_id = self.node_ids.get(&parent).expect("parent should be added earlier"); + + let node_id = self.arena.new_node(node); + parent_id.append(node_id, &mut self.arena); + + let result = self.node_ids.insert(id, node_id); + if result.is_some() { + panic!("duplicate inox node uuid") + } + } + + fn get_internal_node(&self, id: InoxNodeUuid) -> Option<&indextree::Node> { + self.arena.get(*self.node_ids.get(&id)?) + } + + fn get_internal_node_mut(&mut self, id: InoxNodeUuid) -> Option<&mut indextree::Node> { + self.arena.get_mut(*self.node_ids.get(&id)?) + } + + pub fn get_node(&self, id: InoxNodeUuid) -> Option<&InoxNode> { + Some(self.get_internal_node(id)?.get()) + } + + pub fn get_node_mut(&mut self, id: InoxNodeUuid) -> Option<&mut InoxNode> { + Some(self.get_internal_node_mut(id)?.get_mut()) + } +} diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 376b4b1..8eae8b1 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -6,7 +6,6 @@ use crate::math::transform::TransformOffset; use crate::mesh::Mesh; use crate::model::Model; use crate::node::data::{Composite, InoxData, MaskMode, Part}; -use crate::node::tree::InoxNodeTree; use crate::node::InoxNodeUuid; use crate::puppet::Puppet; From ea6d2f7b4325bdbbc99188111023c385b5eb39e4 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Thu, 18 Jul 2024 10:43:16 +0800 Subject: [PATCH 05/34] Absolute transforms of nodes as components. --- inox2d/src/math/transform.rs | 7 ++++++ inox2d/src/puppet.rs | 3 +-- inox2d/src/puppet/transforms.rs | 37 +++++++++++++++++++++++++++ inox2d/src/puppet/tree.rs | 44 +++++++++++++++++++++++++++++++-- 4 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 inox2d/src/puppet/transforms.rs diff --git a/inox2d/src/math/transform.rs b/inox2d/src/math/transform.rs index 33ff865..bf8be7c 100644 --- a/inox2d/src/math/transform.rs +++ b/inox2d/src/math/transform.rs @@ -1,5 +1,12 @@ use glam::{EulerRot, Mat4, Quat, Vec2, Vec3}; +#[derive(Default)] +/// absolute transform +pub struct Transform { + pub mat: Mat4, +} + +/// relative transform #[derive(Debug, Clone, Copy)] pub struct TransformOffset { /// X Y Z diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index ccc32f5..f5a3857 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -1,6 +1,5 @@ pub mod meta; -pub mod tree; -pub mod world; +mod transforms; use std::collections::HashMap; diff --git a/inox2d/src/puppet/transforms.rs b/inox2d/src/puppet/transforms.rs new file mode 100644 index 0000000..e27fff1 --- /dev/null +++ b/inox2d/src/puppet/transforms.rs @@ -0,0 +1,37 @@ +use crate::math::transform::Transform; + +use super::Puppet; + +impl Puppet { + /// give every node a Transform component for the absolute transform, if the puppet is going to be rendered/animated + fn init_node_transforms(&mut self) { + for node in self.nodes.iter() { + self.node_comps.add(node.uuid, Transform::default()); + } + } + + /// Update the puppet's nodes' absolute transforms, by combining transforms + /// from each node's ancestors in a pre-order traversal manner. + pub(crate) fn update_trans(&mut self) { + let root_node = self.nodes.get_node(self.nodes.root_node_id).unwrap(); + + // The root's absolute transform is its relative transform. + let root_trans = root_node.trans_offset.to_matrix(); + let root_trans_comp = self.node_comps.get_mut::(root_node.uuid).unwrap(); + root_trans_comp.mat = root_trans; + + // Pre-order traversal, just the order to ensure that parents are accessed earlier than children + // Skip the root + for node in self.nodes.pre_order_iter().skip(1) { + let base_trans = if node.lock_to_root { + root_trans + } else { + let parent = self.nodes.get_parent(node.uuid); + self.node_comps.get_mut::(parent.uuid).unwrap().mat + }; + + let node_trans_comp = self.node_comps.get_mut::(node.uuid).unwrap(); + node_trans_comp.mat = base_trans * node.trans_offset.to_matrix(); + } + } +} diff --git a/inox2d/src/puppet/tree.rs b/inox2d/src/puppet/tree.rs index ca63afa..4adf19c 100644 --- a/inox2d/src/puppet/tree.rs +++ b/inox2d/src/puppet/tree.rs @@ -5,7 +5,8 @@ use indextree::Arena; use crate::node::{InoxNode, InoxNodeUuid}; pub struct InoxNodeTree { - root_id: indextree::NodeId, + // make this public, instead of replicating all node methods for root, now that callers have the root id + pub root_node_id: InoxNodeUuid, arena: Arena, node_ids: HashMap, } @@ -20,7 +21,7 @@ impl InoxNodeTree { node_ids.insert(id, root_id); Self { - root_id, + root_node_id: id, arena, node_ids, } @@ -53,4 +54,43 @@ impl InoxNodeTree { pub fn get_node_mut(&mut self, id: InoxNodeUuid) -> Option<&mut InoxNode> { Some(self.get_internal_node_mut(id)?.get_mut()) } + + /// order is not guaranteed. use pre_order_iter() for pre-order traversal + pub fn iter(&self) -> impl Iterator { + self.arena.iter().map(|n| { + if n.is_removed() { + panic!("There is a removed node inside the indextree::Arena of the node tree.") + } + n.get() + }) + } + + pub fn pre_order_iter(&self) -> impl Iterator { + let root_id = self.node_ids.get(&self.root_node_id).unwrap(); + root_id + .descendants(&self.arena) + .map(|id| self.arena.get(id).unwrap().get()) + } + + /// WARNING: panicks if called on root + pub fn get_parent(&self, children: InoxNodeUuid) -> &InoxNode { + self.arena + .get( + self.arena + .get(*self.node_ids.get(&children).unwrap()) + .unwrap() + .parent() + .unwrap(), + ) + .unwrap() + .get() + } + + pub fn get_children(&self, parent: InoxNodeUuid) -> impl Iterator { + self.node_ids + .get(&parent) + .unwrap() + .children(&self.arena) + .map(|id| self.arena.get(id).unwrap().get()) + } } From 032181f5cea1e300a0c47bfbe91826d98a27a28b Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Thu, 18 Jul 2024 10:55:25 +0800 Subject: [PATCH 06/34] RenderCtx with components in mind. Init and reading methods. Storing absolute transforms is no longer RenderCtx's job. Put graphics buffer stuff to a seperate file. --- inox2d/src/lib.rs | 1 + inox2d/src/puppet.rs | 9 +- inox2d/src/render.rs | 227 +++++++++------------------- inox2d/src/render/vertex_buffers.rs | 73 +++++++++ 4 files changed, 154 insertions(+), 156 deletions(-) create mode 100644 inox2d/src/render/vertex_buffers.rs diff --git a/inox2d/src/lib.rs b/inox2d/src/lib.rs index 3e171c8..22a1f69 100644 --- a/inox2d/src/lib.rs +++ b/inox2d/src/lib.rs @@ -4,6 +4,7 @@ pub mod model; pub mod node; pub mod params; pub mod puppet; +pub mod render; pub mod texture; pub const INOCHI2D_SPEC_VERSION: &str = "1.0-alpha"; diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index f5a3857..0a4c034 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -1,14 +1,17 @@ pub mod meta; mod transforms; +mod tree; +mod world; use std::collections::HashMap; use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{Param, ParamUuid}; +use crate::render::RenderCtx; use meta::PuppetMeta; -use tree::InoxNodeTree; -use world::World; +pub use tree::InoxNodeTree; +pub use world::World; /// Inochi2D puppet. pub struct Puppet { @@ -18,6 +21,7 @@ pub struct Puppet { physics_ctx: Option>>, pub nodes: InoxNodeTree, pub node_comps: World, + render_ctx: Option, pub(crate) params: HashMap, pub(crate) param_names: HashMap, } @@ -37,6 +41,7 @@ impl Puppet { physics_ctx: None, nodes: InoxNodeTree::new_with_root(root), node_comps: World::new(), + render_ctx: None, params, param_names, } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 8eae8b1..5270add 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,193 +1,111 @@ -use std::collections::HashMap; - -use glam::{vec2, Mat4, Vec2}; - -use crate::math::transform::TransformOffset; -use crate::mesh::Mesh; -use crate::model::Model; -use crate::node::data::{Composite, InoxData, MaskMode, Part}; -use crate::node::InoxNodeUuid; -use crate::puppet::Puppet; - -#[derive(Clone, Debug)] -pub struct VertexBuffers { - pub verts: Vec, - pub uvs: Vec, - pub indices: Vec, - pub deforms: Vec, -} +mod vertex_buffers; -impl Default for VertexBuffers { - fn default() -> Self { - // init with a quad covering the whole viewport - - #[rustfmt::skip] - let verts = vec![ - vec2(-1.0, -1.0), - vec2(-1.0, 1.0), - vec2( 1.0, -1.0), - vec2( 1.0, 1.0), - ]; - - #[rustfmt::skip] - let uvs = vec![ - vec2(0.0, 0.0), - vec2(0.0, 1.0), - vec2(1.0, 0.0), - vec2(1.0, 1.0), - ]; - - #[rustfmt::skip] - let indices = vec![ - 0, 1, 2, - 2, 1, 3, - ]; - - let deforms = vec![Vec2::ZERO; 4]; - - Self { - verts, - uvs, - indices, - deforms, - } - } -} - -impl VertexBuffers { - /// Adds the mesh's vertices and UVs to the buffers and returns its index and vertex offset. - pub fn push(&mut self, mesh: &Mesh) -> (u16, u16) { - let index_offset = self.indices.len() as u16; - let vert_offset = self.verts.len() as u16; +use std::collections::HashMap; - self.verts.extend_from_slice(&mesh.vertices); - self.uvs.extend_from_slice(&mesh.uvs); - self.indices - .extend(mesh.indices.iter().map(|index| index + vert_offset)); - self.deforms - .resize(self.deforms.len() + mesh.vertices.len(), Vec2::ZERO); +use crate::node::{ + components::{Composite, Drawable, TexturedMesh}, + InoxNodeUuid, +}; +use crate::puppet::{InoxNodeTree, World}; - (index_offset, vert_offset) - } -} +use vertex_buffers::VertexBuffers; -#[derive(Debug, Clone)] -pub struct PartRenderCtx { +struct TexturedMeshRenderCtx { pub index_offset: u16, pub vert_offset: u16, pub index_len: usize, pub vert_len: usize, } -#[derive(Debug, Clone)] -pub enum RenderCtxKind { - Node, - Part(PartRenderCtx), +enum NodeRenderCtx { + TexturedMesh(TexturedMeshRenderCtx), Composite(Vec), } -#[derive(Clone, Debug)] -pub struct NodeRenderCtx { - pub trans: Mat4, - pub trans_offset: TransformOffset, - pub kind: RenderCtxKind, -} - -pub type NodeRenderCtxs = HashMap; - -#[derive(Clone, Debug)] pub struct RenderCtx { - pub vertex_buffers: VertexBuffers, + vertex_buffers: VertexBuffers, /// All nodes that need respective draw method calls: /// - including standalone parts and composite parents, /// - excluding plain mesh masks and composite children. - pub root_drawables_zsorted: Vec, - pub node_render_ctxs: NodeRenderCtxs, + root_drawables_zsorted: Vec, + node_render_ctxs: HashMap, } impl RenderCtx { - pub fn new(nodes: &InoxNodeTree) -> Self { + fn new(nodes: &InoxNodeTree, comps: &World) -> Self { let mut vertex_buffers = VertexBuffers::default(); - let mut root_drawables_zsorted: Vec = Vec::new(); let mut node_render_ctxs = HashMap::new(); - - for uuid in nodes.all_node_ids() { - let node = nodes.get_node(uuid).unwrap(); - - node_render_ctxs.insert( - uuid, - NodeRenderCtx { - trans: Mat4::default(), - trans_offset: node.trans_offset, - kind: match node.data { - InoxData::Part(ref part) => { - let (index_offset, vert_offset) = vertex_buffers.push(&part.mesh); - RenderCtxKind::Part(PartRenderCtx { + let mut drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); + + for node in nodes.iter() { + let is_drawable = comps.get::(node.uuid).is_some(); + if is_drawable { + drawable_uuid_zsort_vec.push((node.uuid, node.zsort)); + + let textured_mesh = comps.get::(node.uuid); + let composite = comps.get::(node.uuid); + match (textured_mesh.is_some(), composite.is_some()) { + (true, true) => panic!("A node is both textured mesh and composite."), + (false, false) => panic!("A drawble node is neither textured mesh nor composite."), + (true, false) => { + let textured_mesh = textured_mesh.unwrap(); + let (index_offset, vert_offset) = vertex_buffers.push(&textured_mesh.mesh); + node_render_ctxs.insert( + node.uuid, + NodeRenderCtx::TexturedMesh(TexturedMeshRenderCtx { index_offset, vert_offset, - index_len: part.mesh.indices.len(), - vert_len: part.mesh.vertices.len(), - }) - } - - InoxData::Composite(_) => RenderCtxKind::Composite(nodes.zsorted_children(uuid)), - - _ => RenderCtxKind::Node, - }, - }, - ); - } - - for uuid in nodes.zsorted_root() { - let node = nodes.get_node(uuid).unwrap(); - - match node.data { - InoxData::Part(_) | InoxData::Composite(_) => { - root_drawables_zsorted.push(uuid); - } - - _ => (), + index_len: textured_mesh.mesh.indices.len(), + vert_len: textured_mesh.mesh.vertices.len(), + }), + ); + } + (false, true) => { + // if any of the children is not a drawable or is a composite, we have a problem, but it will error later + let mut zsorted_children_list: Vec = + nodes.get_children(node.uuid).map(|n| n.uuid).collect(); + zsorted_children_list.sort_by(|a, b| { + let zsort_a = nodes.get_node(*a).unwrap().zsort; + let zsort_b = nodes.get_node(*b).unwrap().zsort; + zsort_a.total_cmp(&zsort_b).reverse() + }); + + node_render_ctxs.insert(node.uuid, NodeRenderCtx::Composite(zsorted_children_list)); + } + }; } } + drawable_uuid_zsort_vec.sort_by(|a, b| a.1.total_cmp(&b.1).reverse()); + Self { vertex_buffers, - root_drawables_zsorted, + root_drawables_zsorted: drawable_uuid_zsort_vec.into_iter().map(|p| p.0).collect(), node_render_ctxs, } } -} - -impl Puppet { - /// Update the puppet's nodes' absolute transforms, by combining transforms - /// from each node's ancestors in a pre-order traversal manner. - pub fn update_trans(&mut self) { - let root_node = self.nodes.arena[self.nodes.root].get(); - let node_rctxs = &mut self.render_ctx.node_render_ctxs; - - // The root's absolute transform is its relative transform. - let root_trans = node_rctxs.get(&root_node.uuid).unwrap().trans_offset.to_matrix(); - - // Pre-order traversal, just the order to ensure that parents are accessed earlier than children - // Skip the root - for id in self.nodes.root.descendants(&self.nodes.arena).skip(1) { - let node_index = &self.nodes.arena[id]; - let node = node_index.get(); - - if node.lock_to_root { - let node_render_ctx = node_rctxs.get_mut(&node.uuid).unwrap(); - node_render_ctx.trans = root_trans * node_render_ctx.trans_offset.to_matrix(); - } else { - let parent = &self.nodes.arena[node_index.parent().unwrap()].get(); - let parent_trans = node_rctxs.get(&parent.uuid).unwrap().trans; - let node_render_ctx = node_rctxs.get_mut(&node.uuid).unwrap(); - node_render_ctx.trans = parent_trans * node_render_ctx.trans_offset.to_matrix(); - } - } + fn get_raw_verts(&self) -> &[f32] { + VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.verts) + } + fn get_raw_uvs(&self) -> &[f32] { + VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.uvs) + } + fn get_raw_indices(&self) -> &[u16] { + self.vertex_buffers.indices.as_slice() + } + fn get_raw_deforms(&self) -> &[f32] { + VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.deforms) } } +/* +use crate::mesh::Mesh; +use crate::model::Model; +use crate::node::data::{Composite, InoxData, MaskMode, Part}; +use crate::puppet::Puppet; + + pub trait InoxRenderer where Self: Sized, @@ -375,3 +293,4 @@ impl InoxRendererCommon for T { } } } +*/ diff --git a/inox2d/src/render/vertex_buffers.rs b/inox2d/src/render/vertex_buffers.rs new file mode 100644 index 0000000..3d77d74 --- /dev/null +++ b/inox2d/src/render/vertex_buffers.rs @@ -0,0 +1,73 @@ +use std::mem::transmute; +use std::slice::from_raw_parts; + +use glam::{vec2, Vec2}; + +use crate::node::components::textured_mesh::Mesh; + +pub struct VertexBuffers { + pub verts: Vec, + pub uvs: Vec, + pub indices: Vec, + pub deforms: Vec, +} + +impl Default for VertexBuffers { + fn default() -> Self { + // init with a quad covering the whole viewport + + #[rustfmt::skip] + let verts = vec![ + vec2(-1.0, -1.0), + vec2(-1.0, 1.0), + vec2(1.0, -1.0), + vec2(1.0, 1.0) + ]; + + #[rustfmt::skip] + let uvs = vec![ + vec2(0.0, 0.0), + vec2(0.0, 1.0), + vec2(1.0, 0.0), + vec2(1.0, 1.0) + ]; + + #[rustfmt::skip] + let indices = vec![ + 0, 1, 2, + 2, 1, 3 + ]; + + let deforms = vec![Vec2::ZERO; 4]; + + Self { + verts, + uvs, + indices, + deforms, + } + } +} + +impl VertexBuffers { + /// Adds the mesh's vertices and UVs to the buffers and returns its index and vertex offset. + pub fn push(&mut self, mesh: &Mesh) -> (u16, u16) { + let index_offset = self.indices.len() as u16; + let vert_offset = self.verts.len() as u16; + + self.verts.extend_from_slice(&mesh.vertices); + self.uvs.extend_from_slice(&mesh.uvs); + self.indices + .extend(mesh.indices.iter().map(|index| index + vert_offset)); + self.deforms + .resize(self.deforms.len() + mesh.vertices.len(), Vec2::ZERO); + + (index_offset, vert_offset) + } + + pub fn vec_vec2_as_vec_f32(vector: &[Vec2]) -> &[f32] { + let data_ptr = vector.as_ptr(); + // Safety: data of Vec is aligned to 64 and densely packed with f32 + unsafe { from_raw_parts(transmute::<*const Vec2, *const f32>(data_ptr), vector.len() * 2) } + } +} From 2746a6ccccf9e7580e10c2e8040b4d95757f6593 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Thu, 18 Jul 2024 20:25:54 +0800 Subject: [PATCH 07/34] Componentize render contexts and store them in puppets. Should be easy for incorporating future renderable node types. --- inox2d/src/render.rs | 96 ++++++++++++++++++++++++++++++++------------ 1 file changed, 70 insertions(+), 26 deletions(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 5270add..adb1355 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,7 +1,7 @@ mod vertex_buffers; -use std::collections::HashMap; +use crate::math::transform::Transform; use crate::node::{ components::{Composite, Drawable, TexturedMesh}, InoxNodeUuid, @@ -10,6 +10,54 @@ use crate::puppet::{InoxNodeTree, World}; use vertex_buffers::VertexBuffers; +/// Possible component combinations of a renderable node. +/// +/// Future extensions go here. +enum DrawableKind<'comps> { + TexturedMesh { + transform: &'comps Transform, + drawable: &'comps Drawable, + data: &'comps TexturedMesh, + }, + Composite { + transform: &'comps Transform, + drawable: &'comps Drawable, + data: &'comps Composite, + }, +} + +impl<'comps> DrawableKind<'comps> { + /// Tries to construct a renderable node data pack from the World of components: + /// - `None` if node not renderable. + /// - Panicks if component combinations invalid. + fn new(id: InoxNodeUuid, comps: &'comps World) -> Option { + let drawable = match comps.get::(id) { + Some(drawable) => drawable, + None => return None, + }; + let transform = comps + .get::(id) + .expect("A drawble must have associated Transform."); + let textured_mesh = comps.get::(id); + let composite = comps.get::(id); + + match (textured_mesh.is_some(), composite.is_some()) { + (true, true) => panic!("The drawable has both TexturedMesh and Composite."), + (false, false) => panic!("The drawable has neither TexturedMesh nor Composite."), + (true, false) => Some(DrawableKind::TexturedMesh { + transform, + drawable, + data: textured_mesh.unwrap(), + }), + (false, true) => Some(DrawableKind::Composite { + transform, + drawable, + data: composite.unwrap(), + }), + } + } +} + struct TexturedMeshRenderCtx { pub index_offset: u16, pub vert_offset: u16, @@ -17,9 +65,8 @@ struct TexturedMeshRenderCtx { pub vert_len: usize, } -enum NodeRenderCtx { - TexturedMesh(TexturedMeshRenderCtx), - Composite(Vec), +struct CompositeRenderCtx { + pub zsorted_children_list: Vec, } pub struct RenderCtx { @@ -28,40 +75,38 @@ pub struct RenderCtx { /// - including standalone parts and composite parents, /// - excluding plain mesh masks and composite children. root_drawables_zsorted: Vec, - node_render_ctxs: HashMap, } impl RenderCtx { - fn new(nodes: &InoxNodeTree, comps: &World) -> Self { + /// MODIFIES puppet. In addition to initializing self, installs render contexts in the World of components + fn new(puppet: &mut Puppet) -> Self { + let nodes = &puppet.nodes; + let comps = &mut puppet.node_comps; + let mut vertex_buffers = VertexBuffers::default(); - let mut node_render_ctxs = HashMap::new(); let mut drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); for node in nodes.iter() { - let is_drawable = comps.get::(node.uuid).is_some(); - if is_drawable { + let drawable_kind = DrawableKind::new(node.uuid, comps); + if let Some(drawable_kind) = drawable_kind { drawable_uuid_zsort_vec.push((node.uuid, node.zsort)); - let textured_mesh = comps.get::(node.uuid); - let composite = comps.get::(node.uuid); - match (textured_mesh.is_some(), composite.is_some()) { - (true, true) => panic!("A node is both textured mesh and composite."), - (false, false) => panic!("A drawble node is neither textured mesh nor composite."), - (true, false) => { - let textured_mesh = textured_mesh.unwrap(); - let (index_offset, vert_offset) = vertex_buffers.push(&textured_mesh.mesh); - node_render_ctxs.insert( + match drawable_kind { + DrawableKind::TexturedMesh { data, .. } => { + let (index_offset, vert_offset) = vertex_buffers.push(&data.mesh); + + comps.add( node.uuid, - NodeRenderCtx::TexturedMesh(TexturedMeshRenderCtx { + TexturedMeshRenderCtx { index_offset, vert_offset, - index_len: textured_mesh.mesh.indices.len(), - vert_len: textured_mesh.mesh.vertices.len(), - }), + index_len: data.mesh.indices.len(), + vert_len: data.mesh.vertices.len(), + }, ); } - (false, true) => { - // if any of the children is not a drawable or is a composite, we have a problem, but it will error later + DrawableKind::Composite { .. } => { + // if any of the children is not a drawable, we have a problem, but it will error later let mut zsorted_children_list: Vec = nodes.get_children(node.uuid).map(|n| n.uuid).collect(); zsorted_children_list.sort_by(|a, b| { @@ -70,7 +115,7 @@ impl RenderCtx { zsort_a.total_cmp(&zsort_b).reverse() }); - node_render_ctxs.insert(node.uuid, NodeRenderCtx::Composite(zsorted_children_list)); + comps.add(node.uuid, CompositeRenderCtx { zsorted_children_list }); } }; } @@ -81,7 +126,6 @@ impl RenderCtx { Self { vertex_buffers, root_drawables_zsorted: drawable_uuid_zsort_vec.into_iter().map(|p| p.0).collect(), - node_render_ctxs, } } From 9bd7c4d7f45e80f8ff517f30727c5b75f80ecb0c Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Thu, 18 Jul 2024 22:46:13 +0800 Subject: [PATCH 08/34] Complete render.rs rewrite. Get required data from components instead. Changes to the renderer traits: - remove `render()` - `on_begin_mask()` -> `on_begin_masks()` - `set_mask_mode()` -> `on_begin_mask()` - `draw_mesh_self()` -> `draw_textured_mesh_content()` - `draw_part()` -> `draw_drawable()` --- inox2d/src/puppet.rs | 2 +- inox2d/src/render.rs | 263 ++++++++++++++++++++++++------------------- 2 files changed, 151 insertions(+), 114 deletions(-) diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 0a4c034..4866def 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -21,7 +21,7 @@ pub struct Puppet { physics_ctx: Option>>, pub nodes: InoxNodeTree, pub node_comps: World, - render_ctx: Option, + pub(crate) render_ctx: Option, pub(crate) params: HashMap, pub(crate) param_names: HashMap, } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index adb1355..58a4247 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,12 +1,17 @@ mod vertex_buffers; +use glam::Mat4; use crate::math::transform::Transform; +use crate::model::Model; use crate::node::{ - components::{Composite, Drawable, TexturedMesh}, + components::{ + drawable::{Mask, Masks}, + Composite, Drawable, TexturedMesh, + }, InoxNodeUuid, }; -use crate::puppet::{InoxNodeTree, World}; +use crate::puppet::{Puppet, World}; use vertex_buffers::VertexBuffers; @@ -58,14 +63,14 @@ impl<'comps> DrawableKind<'comps> { } } -struct TexturedMeshRenderCtx { +pub struct TexturedMeshRenderCtx { pub index_offset: u16, pub vert_offset: u16, pub index_len: usize, pub vert_len: usize, } -struct CompositeRenderCtx { +pub struct CompositeRenderCtx { pub zsorted_children_list: Vec, } @@ -106,9 +111,17 @@ impl RenderCtx { ); } DrawableKind::Composite { .. } => { - // if any of the children is not a drawable, we have a problem, but it will error later - let mut zsorted_children_list: Vec = - nodes.get_children(node.uuid).map(|n| n.uuid).collect(); + // exclude non-drawable children + let mut zsorted_children_list: Vec = nodes + .get_children(node.uuid) + .filter_map(|n| { + if DrawableKind::new(n.uuid, comps).is_some() { + Some(n.uuid) + } else { + None + } + }) + .collect(); zsorted_children_list.sort_by(|a, b| { let zsort_a = nodes.get_node(*a).unwrap().zsort; let zsort_b = nodes.get_node(*b).unwrap().zsort; @@ -143,12 +156,16 @@ impl RenderCtx { } } -/* -use crate::mesh::Mesh; -use crate::model::Model; -use crate::node::data::{Composite, InoxData, MaskMode, Part}; -use crate::puppet::Puppet; +impl Puppet { + pub fn init_render_ctx(&mut self) { + if self.render_ctx.is_some() { + panic!("RenderCtx already initialized."); + } + let render_ctx = RenderCtx::new(self); + self.render_ctx = Some(render_ctx); + } +} pub trait InoxRenderer where @@ -169,10 +186,6 @@ where /// Initiate one render pass. fn on_begin_scene(&self); - /// The render pass. - /// - /// Logical error if this puppet is not from the latest prepared model. - fn render(&self, puppet: &Puppet); /// Finish one render pass. fn on_end_scene(&self); /// Actually make results visible, e.g. on a screen/texture. @@ -181,55 +194,70 @@ where /// Begin masking. /// /// Clear and start writing to the stencil buffer, lock the color buffer. - fn on_begin_mask(&self, has_mask: bool); - /// The following draw calls consist of a mask or dodge mask. - fn set_mask_mode(&self, dodge: bool); + fn on_begin_masks(&self, masks: &Masks); + /// Get prepared for rendering a singular Mask. + fn on_begin_mask(&self, mask: &Mask); + /// Get prepared for rendering masked content. + /// /// Read only from the stencil buffer, unlock the color buffer. fn on_begin_masked_content(&self); + /// End masking. + /// /// Disable the stencil buffer. fn on_end_mask(&self); - /// Draw contents of a mesh-defined plain region. - // TODO: plain mesh (usually for mesh masks) not implemented - fn draw_mesh_self(&self, as_mask: bool, camera: &Mat4); - - /// Draw contents of a part. - // TODO: Merging of Part and PartRenderCtx? - // TODO: Inclusion of NodeRenderCtx into Part? - fn draw_part_self( + /// Draw TexturedMesh content. + /// + /// TODO: TexturedMesh without any texture (usually for mesh masks) not implemented + fn draw_textured_mesh_content( &self, as_mask: bool, camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, + trans: &Transform, + drawable: &Drawable, + textured_mesh: &TexturedMesh, + render_ctx: &TexturedMeshRenderCtx, ); /// When something needs to happen before drawing to the composite buffers. - fn begin_composite_content(&self); + fn begin_composite_content( + &self, + as_mask: bool, + drawable: &Drawable, + composite: &Composite, + render_ctx: &CompositeRenderCtx, + ); /// Transfer content from composite buffers to normal buffers. - fn finish_composite_content(&self, as_mask: bool, composite: &Composite); + fn finish_composite_content( + &self, + as_mask: bool, + drawable: &Drawable, + composite: &Composite, + render_ctx: &CompositeRenderCtx, + ); } -pub trait InoxRendererCommon { - /// Draw one part, with its content properly masked. - fn draw_part( +trait InoxRendererCommon { + /// Draw a Drawable, which is potentially masked. + fn draw_drawable<'comps>( &self, + as_mask: bool, camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, - puppet: &Puppet, + drawable_kind: &'comps DrawableKind, + comps: &'comps World, + id: InoxNodeUuid, ); /// Draw one composite. - fn draw_composite( + fn draw_composite<'comps>( &self, as_mask: bool, camera: &Mat4, - composite: &Composite, - puppet: &Puppet, - children: &[InoxNodeUuid], + trans: &'comps Transform, + drawable: &'comps Drawable, + composite: &'comps Composite, + render_ctx: &'comps CompositeRenderCtx, + comps: &'comps World, ); /// Iterate over top-level drawables (excluding masks) in zsort order, @@ -240,101 +268,110 @@ pub trait InoxRendererCommon { } impl InoxRendererCommon for T { - fn draw_part( + fn draw_drawable<'comps>( &self, + as_mask: bool, camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, - puppet: &Puppet, + drawable_kind: &'comps DrawableKind, + comps: &'comps World, + id: InoxNodeUuid, ) { - let masks = &part.draw_state.masks; - if !masks.is_empty() { - self.on_begin_mask(part.draw_state.has_masks()); - for mask in &part.draw_state.masks { - self.set_mask_mode(mask.mode == MaskMode::Dodge); - - let mask_node = puppet.nodes.get_node(mask.source).unwrap(); - let mask_node_render_ctx = &puppet.render_ctx.node_render_ctxs[&mask.source]; - - match (&mask_node.data, &mask_node_render_ctx.kind) { - (InoxData::Part(ref mask_part), RenderCtxKind::Part(ref mask_part_render_ctx)) => { - self.draw_part_self(true, camera, mask_node_render_ctx, mask_part, mask_part_render_ctx); - } + let masks = match drawable_kind { + DrawableKind::TexturedMesh { drawable, .. } => &drawable.masks, + DrawableKind::Composite { drawable, .. } => &drawable.masks, + }; - (InoxData::Composite(ref mask_composite), RenderCtxKind::Composite(ref mask_children)) => { - self.draw_composite(true, camera, mask_composite, puppet, mask_children); - } + let mut has_masks = false; + if let Some(ref masks) = masks { + has_masks = true; + self.on_begin_masks(masks); + for mask in &masks.masks { + self.on_begin_mask(mask); - _ => { - // This match block clearly is sign that the data structure needs rework - todo!(); - } - } + let mask_id = mask.source; + let mask_drawable_kind = DrawableKind::new(mask_id, comps).expect("A Mask source must be a Drawable."); + + self.draw_drawable(true, camera, &mask_drawable_kind, comps, mask_id); } self.on_begin_masked_content(); - self.draw_part_self(false, camera, node_render_ctx, part, part_render_ctx); + } + + match drawable_kind { + DrawableKind::TexturedMesh { + transform, + drawable, + data, + } => self.draw_textured_mesh_content(as_mask, camera, transform, drawable, data, comps.get(id).unwrap()), + DrawableKind::Composite { + transform, + drawable, + data, + } => self.draw_composite( + as_mask, + camera, + transform, + drawable, + data, + comps.get(id).unwrap(), + comps, + ), + } + + if has_masks { self.on_end_mask(); - } else { - self.draw_part_self(false, camera, node_render_ctx, part, part_render_ctx); } } - fn draw_composite( + fn draw_composite<'comps>( &self, as_mask: bool, camera: &Mat4, - comp: &Composite, - puppet: &Puppet, - children: &[InoxNodeUuid], + _trans: &'comps Transform, + drawable: &'comps Drawable, + composite: &'comps Composite, + render_ctx: &'comps CompositeRenderCtx, + comps: &'comps World, ) { - if children.is_empty() { + if render_ctx.zsorted_children_list.is_empty() { // Optimization: Nothing to be drawn, skip context switching return; } - self.begin_composite_content(); - - for &uuid in children { - let node = puppet.nodes.get_node(uuid).unwrap(); - let node_render_ctx = &puppet.render_ctx.node_render_ctxs[&uuid]; - - if let (InoxData::Part(ref part), RenderCtxKind::Part(ref part_render_ctx)) = - (&node.data, &node_render_ctx.kind) - { - if as_mask { - self.draw_part_self(true, camera, node_render_ctx, part, part_render_ctx); - } else { - self.draw_part(camera, node_render_ctx, part, part_render_ctx, puppet); - } - } else { - // composite inside composite simply cannot happen + self.begin_composite_content(as_mask, drawable, composite, render_ctx); + + for uuid in &render_ctx.zsorted_children_list { + let drawable_kind = + DrawableKind::new(*uuid, comps).expect("All children in zsorted_children_list should be a Drawable."); + match drawable_kind { + DrawableKind::TexturedMesh { + transform, + drawable, + data, + } => self.draw_textured_mesh_content( + as_mask, + camera, + transform, // Note: this is already the absolute transform, no need to multiply again + drawable, + data, + comps.get(*uuid).unwrap(), + ), + DrawableKind::Composite { .. } => panic!("Composite inside Composite not allowed."), } } - self.finish_composite_content(as_mask, comp); + self.finish_composite_content(as_mask, drawable, composite, render_ctx); } fn draw(&self, camera: &Mat4, puppet: &Puppet) { - for &uuid in &puppet.render_ctx.root_drawables_zsorted { - let node = puppet.nodes.get_node(uuid).unwrap(); - let node_render_ctx = &puppet.render_ctx.node_render_ctxs[&uuid]; - - match (&node.data, &node_render_ctx.kind) { - (InoxData::Part(ref part), RenderCtxKind::Part(ref part_render_ctx)) => { - self.draw_part(camera, node_render_ctx, part, part_render_ctx, puppet); - } - - (InoxData::Composite(ref composite), RenderCtxKind::Composite(ref children)) => { - self.draw_composite(false, camera, composite, puppet, children); - } - - _ => { - // This clearly is sign that the data structure needs rework - todo!(); - } - } + for uuid in &puppet + .render_ctx + .as_ref() + .expect("RenderCtx of puppet must be initialized before calling draw().") + .root_drawables_zsorted + { + let drawable_kind = DrawableKind::new(*uuid, &puppet.node_comps) + .expect("Every node in root_drawables_zsorted must be a Drawable."); + self.draw_drawable(false, camera, &drawable_kind, &puppet.node_comps, *uuid); } } } -*/ From 08cccb6c1be756a5b735a04ad8bb2c5168523d75 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Fri, 19 Jul 2024 18:13:52 +0800 Subject: [PATCH 09/34] Comments for `render.rs` and lighter interfaces. --- inox2d/src/puppet.rs | 25 ++++- inox2d/src/render.rs | 257 +++++++++++++++++++------------------------ 2 files changed, 133 insertions(+), 149 deletions(-) diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 4866def..bee76a4 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -18,16 +18,21 @@ pub struct Puppet { pub meta: PuppetMeta, physics: PuppetPhysics, // TODO: define the actual ctx - physics_ctx: Option>>, - pub nodes: InoxNodeTree, - pub node_comps: World, - pub(crate) render_ctx: Option, + pub(crate) nodes: InoxNodeTree, + pub(crate) node_comps: World, + /// Context for rendering this puppet. See `.init_render_ctx()`. + pub render_ctx: Option, pub(crate) params: HashMap, pub(crate) param_names: HashMap, } impl Puppet { - pub fn new(meta: PuppetMeta, physics: PuppetPhysics, root: InoxNode, named_params: HashMap) -> Self { + pub(crate) fn new( + meta: PuppetMeta, + physics: PuppetPhysics, + root: InoxNode, + named_params: HashMap, + ) -> Self { let mut params = HashMap::new(); let mut param_names = HashMap::new(); for (name, param) in named_params { @@ -46,6 +51,16 @@ impl Puppet { param_names, } } + + /// Call this on a freshly loaded puppet if rendering is needed. Panicks on second call. + pub fn init_render_ctx(&mut self) { + if self.render_ctx.is_some() { + panic!("RenderCtx already initialized."); + } + + let render_ctx = RenderCtx::new(self); + self.render_ctx = Some(render_ctx); + } } /// Global physics parameters for the puppet. diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 58a4247..9e27ad1 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -19,16 +19,22 @@ use vertex_buffers::VertexBuffers; /// /// Future extensions go here. enum DrawableKind<'comps> { - TexturedMesh { - transform: &'comps Transform, - drawable: &'comps Drawable, - data: &'comps TexturedMesh, - }, - Composite { - transform: &'comps Transform, - drawable: &'comps Drawable, - data: &'comps Composite, - }, + TexturedMesh(TexturedMeshComponents<'comps>), + Composite(CompositeComponents<'comps>), +} + +/// Pack of components for a TexturedMesh. "Part" in Inochi2D terms. +pub struct TexturedMeshComponents<'comps> { + pub transform: &'comps Transform, + pub drawable: &'comps Drawable, + pub data: &'comps TexturedMesh, +} + +/// Pack of components for a Composite node. +pub struct CompositeComponents<'comps> { + pub transform: &'comps Transform, + pub drawable: &'comps Drawable, + pub data: &'comps Composite, } impl<'comps> DrawableKind<'comps> { @@ -49,20 +55,25 @@ impl<'comps> DrawableKind<'comps> { match (textured_mesh.is_some(), composite.is_some()) { (true, true) => panic!("The drawable has both TexturedMesh and Composite."), (false, false) => panic!("The drawable has neither TexturedMesh nor Composite."), - (true, false) => Some(DrawableKind::TexturedMesh { + (true, false) => Some(DrawableKind::TexturedMesh(TexturedMeshComponents { transform, drawable, data: textured_mesh.unwrap(), - }), - (false, true) => Some(DrawableKind::Composite { + })), + (false, true) => Some(DrawableKind::Composite(CompositeComponents { transform, drawable, data: composite.unwrap(), - }), + })), } } } +/// Additional info per node for rendering a TexturedMesh: +/// - offset and length of array for mesh point coordinates +/// - offset and length of array for indices of mesh points defining the mesh +/// +/// inside `puppet.render_ctx_vertex_buffers`. pub struct TexturedMeshRenderCtx { pub index_offset: u16, pub vert_offset: u16, @@ -70,21 +81,24 @@ pub struct TexturedMeshRenderCtx { pub vert_len: usize, } +/// Additional info per node for rendering a Composite. pub struct CompositeRenderCtx { pub zsorted_children_list: Vec, } +/// Additional struct attached to a puppet for rendering. pub struct RenderCtx { + /// General compact data buffers for interfacing with the GPU. vertex_buffers: VertexBuffers, /// All nodes that need respective draw method calls: /// - including standalone parts and composite parents, - /// - excluding plain mesh masks and composite children. + /// - excluding (TODO: plain mesh masks) and composite children. root_drawables_zsorted: Vec, } impl RenderCtx { /// MODIFIES puppet. In addition to initializing self, installs render contexts in the World of components - fn new(puppet: &mut Puppet) -> Self { + pub(super) fn new(puppet: &mut Puppet) -> Self { let nodes = &puppet.nodes; let comps = &mut puppet.node_comps; @@ -97,16 +111,16 @@ impl RenderCtx { drawable_uuid_zsort_vec.push((node.uuid, node.zsort)); match drawable_kind { - DrawableKind::TexturedMesh { data, .. } => { - let (index_offset, vert_offset) = vertex_buffers.push(&data.mesh); + DrawableKind::TexturedMesh(components) => { + let (index_offset, vert_offset) = vertex_buffers.push(&components.data.mesh); comps.add( node.uuid, TexturedMeshRenderCtx { index_offset, vert_offset, - index_len: data.mesh.indices.len(), - vert_len: data.mesh.vertices.len(), + index_len: components.data.mesh.indices.len(), + vert_len: components.data.mesh.vertices.len(), }, ); } @@ -142,143 +156,114 @@ impl RenderCtx { } } - fn get_raw_verts(&self) -> &[f32] { + /// Memory layout: `[[x, y], [x, y], ...]` + pub fn get_raw_verts(&self) -> &[f32] { VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.verts) } - fn get_raw_uvs(&self) -> &[f32] { + /// Memory layout: `[[x, y], [x, y], ...]` + pub fn get_raw_uvs(&self) -> &[f32] { VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.uvs) } - fn get_raw_indices(&self) -> &[u16] { + /// Memory layout: `[[i0, i1, i2], [i0, i1, i2], ...]` + pub fn get_raw_indices(&self) -> &[u16] { self.vertex_buffers.indices.as_slice() } - fn get_raw_deforms(&self) -> &[f32] { + /// Memory layout: `[[dx, dy], [dx, dy], ...]` + pub fn get_raw_deforms(&self) -> &[f32] { VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.deforms) } } -impl Puppet { - pub fn init_render_ctx(&mut self) { - if self.render_ctx.is_some() { - panic!("RenderCtx already initialized."); - } - - let render_ctx = RenderCtx::new(self); - self.render_ctx = Some(render_ctx); - } -} - +/// Same as the reference Inochi2D implementation, Inox2D also aims for a "bring your own rendering backend" design. +/// A custom backend shall implement this trait. +/// +/// It is perfectly fine that the trait implementation does not contain everything needed to display a puppet as: +/// - The renderer may not be directly rendering to the screen for flexibility. +/// - The renderer may want platform-specific optimizations, e.g. batching, and the provided implementation is merely for collecting puppet info. +/// - The renderer may be a debug/just-for-fun renderer intercepting draw calls for other purposes. +/// +/// Either way, the point is Inox2D will implement a `draw()` method for any `impl InoxRenderer`, dispatching calls based on puppet structure according to Inochi2D standard. pub trait InoxRenderer where Self: Sized, { type Error; - /// For any model-specific setup, e.g. creating buffers with specific sizes. + /// Create a renderer for one model. /// - /// After this step, the provided model should be renderable. - fn prepare(&mut self, model: &Model) -> Result<(), Self::Error>; + /// Ref impl: Upload textures. + fn new(model: &Model) -> Result; - /// Resize the renderer's viewport. - fn resize(&mut self, w: u32, h: u32); - - /// Clear the canvas. - fn clear(&self); - - /// Initiate one render pass. - fn on_begin_scene(&self); - /// Finish one render pass. - fn on_end_scene(&self); - /// Actually make results visible, e.g. on a screen/texture. - fn draw_scene(&self); + /// Sets a quaternion that can translate, rotate and scale the whole puppet. + fn set_camera(&mut self, camera: &Mat4); + /// Returns the quaternion set by `.set_camera()`. + fn camera(&self) -> &Mat4; /// Begin masking. /// - /// Clear and start writing to the stencil buffer, lock the color buffer. + /// Ref impl: Clear and start writing to the stencil buffer, lock the color buffer. fn on_begin_masks(&self, masks: &Masks); /// Get prepared for rendering a singular Mask. fn on_begin_mask(&self, mask: &Mask); /// Get prepared for rendering masked content. /// - /// Read only from the stencil buffer, unlock the color buffer. + /// Ref impl: Read only from the stencil buffer, unlock the color buffer. fn on_begin_masked_content(&self); /// End masking. /// - /// Disable the stencil buffer. + /// Ref impl: Disable the stencil buffer. fn on_end_mask(&self); /// Draw TexturedMesh content. - /// - /// TODO: TexturedMesh without any texture (usually for mesh masks) not implemented + // TODO: TexturedMesh without any texture (usually for mesh masks)? fn draw_textured_mesh_content( &self, as_mask: bool, - camera: &Mat4, - trans: &Transform, - drawable: &Drawable, - textured_mesh: &TexturedMesh, + components: &TexturedMeshComponents, render_ctx: &TexturedMeshRenderCtx, ); - /// When something needs to happen before drawing to the composite buffers. - fn begin_composite_content( - &self, - as_mask: bool, - drawable: &Drawable, - composite: &Composite, - render_ctx: &CompositeRenderCtx, - ); - /// Transfer content from composite buffers to normal buffers. + /// Begin compositing. Get prepared for rendering children of a Composite. + /// + /// Ref impl: Prepare composite buffers. + fn begin_composite_content(&self, as_mask: bool, components: &CompositeComponents, render_ctx: &CompositeRenderCtx); + /// End compositing. + /// + /// Ref impl: Transfer content from composite buffers to normal buffers. fn finish_composite_content( &self, as_mask: bool, - drawable: &Drawable, - composite: &Composite, + components: &CompositeComponents, render_ctx: &CompositeRenderCtx, ); } trait InoxRendererCommon { /// Draw a Drawable, which is potentially masked. - fn draw_drawable<'comps>( - &self, - as_mask: bool, - camera: &Mat4, - drawable_kind: &'comps DrawableKind, - comps: &'comps World, - id: InoxNodeUuid, - ); + fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid); - /// Draw one composite. - fn draw_composite<'comps>( + /// Draw one composite. `components` must be referencing `comps`. + fn draw_composite( &self, as_mask: bool, - camera: &Mat4, - trans: &'comps Transform, - drawable: &'comps Drawable, - composite: &'comps Composite, - render_ctx: &'comps CompositeRenderCtx, - comps: &'comps World, + comps: &World, + components: &CompositeComponents, + render_ctx: &CompositeRenderCtx, ); /// Iterate over top-level drawables (excluding masks) in zsort order, /// and make draw calls correspondingly. /// /// This effectively draws the complete puppet. - fn draw(&self, camera: &Mat4, puppet: &Puppet); + fn draw(&self, puppet: &Puppet); } impl InoxRendererCommon for T { - fn draw_drawable<'comps>( - &self, - as_mask: bool, - camera: &Mat4, - drawable_kind: &'comps DrawableKind, - comps: &'comps World, - id: InoxNodeUuid, - ) { + fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid) { + let drawable_kind = DrawableKind::new(id, comps).expect("Node must be a Drawable."); let masks = match drawable_kind { - DrawableKind::TexturedMesh { drawable, .. } => &drawable.masks, - DrawableKind::Composite { drawable, .. } => &drawable.masks, + DrawableKind::TexturedMesh(ref components) => &components.drawable.masks, + DrawableKind::Composite(ref components) => &components.drawable.masks, }; let mut has_masks = false; @@ -288,33 +273,18 @@ impl InoxRendererCommon for T { for mask in &masks.masks { self.on_begin_mask(mask); - let mask_id = mask.source; - let mask_drawable_kind = DrawableKind::new(mask_id, comps).expect("A Mask source must be a Drawable."); - - self.draw_drawable(true, camera, &mask_drawable_kind, comps, mask_id); + self.draw_drawable(true, comps, mask.source); } self.on_begin_masked_content(); } match drawable_kind { - DrawableKind::TexturedMesh { - transform, - drawable, - data, - } => self.draw_textured_mesh_content(as_mask, camera, transform, drawable, data, comps.get(id).unwrap()), - DrawableKind::Composite { - transform, - drawable, - data, - } => self.draw_composite( - as_mask, - camera, - transform, - drawable, - data, - comps.get(id).unwrap(), - comps, - ), + DrawableKind::TexturedMesh(ref components) => { + self.draw_textured_mesh_content(as_mask, components, comps.get(id).unwrap()) + } + DrawableKind::Composite(ref components) => { + self.draw_composite(as_mask, comps, components, comps.get(id).unwrap()) + } } if has_masks { @@ -322,56 +292,55 @@ impl InoxRendererCommon for T { } } - fn draw_composite<'comps>( + fn draw_composite( &self, as_mask: bool, - camera: &Mat4, - _trans: &'comps Transform, - drawable: &'comps Drawable, - composite: &'comps Composite, - render_ctx: &'comps CompositeRenderCtx, - comps: &'comps World, + comps: &World, + components: &CompositeComponents, + render_ctx: &CompositeRenderCtx, ) { if render_ctx.zsorted_children_list.is_empty() { // Optimization: Nothing to be drawn, skip context switching return; } - self.begin_composite_content(as_mask, drawable, composite, render_ctx); + self.begin_composite_content(as_mask, components, render_ctx); for uuid in &render_ctx.zsorted_children_list { let drawable_kind = DrawableKind::new(*uuid, comps).expect("All children in zsorted_children_list should be a Drawable."); match drawable_kind { - DrawableKind::TexturedMesh { - transform, - drawable, - data, - } => self.draw_textured_mesh_content( - as_mask, - camera, - transform, // Note: this is already the absolute transform, no need to multiply again - drawable, - data, - comps.get(*uuid).unwrap(), - ), + DrawableKind::TexturedMesh(components) => { + self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap()) + } DrawableKind::Composite { .. } => panic!("Composite inside Composite not allowed."), } } - self.finish_composite_content(as_mask, drawable, composite, render_ctx); + self.finish_composite_content(as_mask, components, render_ctx); } - fn draw(&self, camera: &Mat4, puppet: &Puppet) { + fn draw(&self, puppet: &Puppet) { for uuid in &puppet .render_ctx .as_ref() .expect("RenderCtx of puppet must be initialized before calling draw().") .root_drawables_zsorted { - let drawable_kind = DrawableKind::new(*uuid, &puppet.node_comps) - .expect("Every node in root_drawables_zsorted must be a Drawable."); - self.draw_drawable(false, camera, &drawable_kind, &puppet.node_comps, *uuid); + self.draw_drawable(false, &puppet.node_comps, *uuid); } } } + +/// Dispatches draw calls for all nodes of `puppet` +/// - with provided renderer implementation, +/// - in Inochi2D standard defined order. +/// +/// This does not guarantee the display of a puppet on screen due to these possible reasons: +/// - Only provided `InoxRenderer` method implementations are called. +/// For example, maybe the caller still need to transfer content from a texture buffer to the screen surface buffer. +/// - The provided `InoxRender` implementation is wrong. +/// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. +pub fn draw(renderer: &T, puppet: &Puppet) { + renderer.draw(puppet); +} From 3cc6f6489dad237ea3287c80fb3769bc4ee4b222 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Fri, 19 Jul 2024 21:29:40 +0800 Subject: [PATCH 10/34] Pass `InoxNodeUuid` of the node in question to draw calls in case some backend gets smart. For example reusing some results for a same node. --- inox2d/src/render.rs | 39 +++++++++++++++++---------------------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 9e27ad1..27ae03d 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -221,12 +221,19 @@ where as_mask: bool, components: &TexturedMeshComponents, render_ctx: &TexturedMeshRenderCtx, + id: InoxNodeUuid, ); /// Begin compositing. Get prepared for rendering children of a Composite. /// /// Ref impl: Prepare composite buffers. - fn begin_composite_content(&self, as_mask: bool, components: &CompositeComponents, render_ctx: &CompositeRenderCtx); + fn begin_composite_content( + &self, + as_mask: bool, + components: &CompositeComponents, + render_ctx: &CompositeRenderCtx, + id: InoxNodeUuid, + ); /// End compositing. /// /// Ref impl: Transfer content from composite buffers to normal buffers. @@ -235,6 +242,7 @@ where as_mask: bool, components: &CompositeComponents, render_ctx: &CompositeRenderCtx, + id: InoxNodeUuid, ); } @@ -243,13 +251,7 @@ trait InoxRendererCommon { fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid); /// Draw one composite. `components` must be referencing `comps`. - fn draw_composite( - &self, - as_mask: bool, - comps: &World, - components: &CompositeComponents, - render_ctx: &CompositeRenderCtx, - ); + fn draw_composite(&self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid); /// Iterate over top-level drawables (excluding masks) in zsort order, /// and make draw calls correspondingly. @@ -280,11 +282,9 @@ impl InoxRendererCommon for T { match drawable_kind { DrawableKind::TexturedMesh(ref components) => { - self.draw_textured_mesh_content(as_mask, components, comps.get(id).unwrap()) - } - DrawableKind::Composite(ref components) => { - self.draw_composite(as_mask, comps, components, comps.get(id).unwrap()) + self.draw_textured_mesh_content(as_mask, components, comps.get(id).unwrap(), id) } + DrawableKind::Composite(ref components) => self.draw_composite(as_mask, comps, components, id), } if has_masks { @@ -292,32 +292,27 @@ impl InoxRendererCommon for T { } } - fn draw_composite( - &self, - as_mask: bool, - comps: &World, - components: &CompositeComponents, - render_ctx: &CompositeRenderCtx, - ) { + fn draw_composite(&self, as_mask: bool, comps: &World, components: &CompositeComponents, id: InoxNodeUuid) { + let render_ctx = comps.get::(id).unwrap(); if render_ctx.zsorted_children_list.is_empty() { // Optimization: Nothing to be drawn, skip context switching return; } - self.begin_composite_content(as_mask, components, render_ctx); + self.begin_composite_content(as_mask, components, render_ctx, id); for uuid in &render_ctx.zsorted_children_list { let drawable_kind = DrawableKind::new(*uuid, comps).expect("All children in zsorted_children_list should be a Drawable."); match drawable_kind { DrawableKind::TexturedMesh(components) => { - self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap()) + self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) } DrawableKind::Composite { .. } => panic!("Composite inside Composite not allowed."), } } - self.finish_composite_content(as_mask, components, render_ctx); + self.finish_composite_content(as_mask, components, render_ctx, id); } fn draw(&self, puppet: &Puppet) { From 1cfa783b87ebcf4f6306c9b6b23044af2c32d8fd Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sat, 20 Jul 2024 18:30:51 +0800 Subject: [PATCH 11/34] Deformations by parameters. Enum `Deform` and `DeformSrc` to get prepared for Meshgroup implementation. One `DeformStack` per deform-able node for combining multiple deforms in a controlled way. Drawable related stuff, especially the drawable kind determination logic, moved to its own places from `render.rs`, as apparently other systems (parameters for example) may want to know is a node a drawable. --- inox2d/src/math.rs | 1 + inox2d/src/math/deform.rs | 24 +++++++ inox2d/src/node.rs | 1 + inox2d/src/node/components.rs | 5 ++ inox2d/src/node/components/deform_stack.rs | 81 ++++++++++++++++++++++ inox2d/src/node/drawables.rs | 62 +++++++++++++++++ inox2d/src/params.rs | 75 +++++++++++++------- inox2d/src/render.rs | 77 +++++--------------- 8 files changed, 242 insertions(+), 84 deletions(-) create mode 100644 inox2d/src/math/deform.rs create mode 100644 inox2d/src/node/components/deform_stack.rs create mode 100644 inox2d/src/node/drawables.rs diff --git a/inox2d/src/math.rs b/inox2d/src/math.rs index a6ad02b..952b312 100644 --- a/inox2d/src/math.rs +++ b/inox2d/src/math.rs @@ -1,4 +1,5 @@ pub mod camera; +pub mod deform; pub mod interp; pub mod matrix; pub mod transform; diff --git a/inox2d/src/math/deform.rs b/inox2d/src/math/deform.rs new file mode 100644 index 0000000..72dd220 --- /dev/null +++ b/inox2d/src/math/deform.rs @@ -0,0 +1,24 @@ +use glam::Vec2; + +/// Different kinds of deform. +// TODO: Meshgroup. +pub(crate) enum Deform { + /// Specifying a displacement for every vertex. + Direct(Vec), +} + +/// Element-wise add direct deforms up and write result. +pub(crate) fn linear_combine<'deforms>(direct_deforms: impl Iterator>, result: &mut [Vec2]) { + result.iter_mut().for_each(|deform| *deform = Vec2::ZERO); + + for direct_deform in direct_deforms { + if direct_deform.len() != result.len() { + panic!("Trying to combine direct deformations with wrong dimensions."); + } + + result + .iter_mut() + .zip(direct_deform.iter()) + .for_each(|(sum, addition)| *sum += *addition); + } +} diff --git a/inox2d/src/node.rs b/inox2d/src/node.rs index bcdee49..432f3dc 100644 --- a/inox2d/src/node.rs +++ b/inox2d/src/node.rs @@ -1,4 +1,5 @@ pub mod components; +pub mod drawables; use crate::math::transform::TransformOffset; diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index c4b1eec..da5f282 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -12,7 +12,12 @@ pub mod drawable; pub mod simple_physics; pub mod textured_mesh; +/// Internal component solving for deforms of a node. +pub(crate) mod deform_stack; + pub use composite::Composite; pub use drawable::Drawable; pub use simple_physics::SimplePhysics; pub use textured_mesh::TexturedMesh; + +pub(crate) use deform_stack::DeformStack; diff --git a/inox2d/src/node/components/deform_stack.rs b/inox2d/src/node/components/deform_stack.rs new file mode 100644 index 0000000..aaa12aa --- /dev/null +++ b/inox2d/src/node/components/deform_stack.rs @@ -0,0 +1,81 @@ +use core::mem::swap; +use std::collections::HashMap; + +use glam::Vec2; + +use crate::math::deform::{linear_combine, Deform}; +use crate::node::InoxNodeUuid; +use crate::params::ParamUuid; +use crate::puppet::{InoxNodeTree, World}; + +/// Source of a deform. +#[derive(Hash, PartialEq, Eq, Copy, Clone)] +pub(crate) enum DeformSrc { + Param(ParamUuid), + Node(InoxNodeUuid), +} + +/// Storing deforms specified by multiple sources to apply on one node for one frame. +/// +/// Despite the name (this is respecting the ref impl), this is not in any way a stack. +/// The order of deforms being applied, or more generally speaking, the way multiple deforms adds up to be a single one, needs to be defined according to the spec. +pub(crate) struct DeformStack { + /// this is a component so cannot use generics for the length. + deform_len: usize, + /// map of (src, (enabled, Deform)). + /// On reset, only set enabled to false instead of clearing the map, as deforms from same sources tend to come in every frame. + stack: HashMap, +} + +impl DeformStack { + pub(crate) fn new(deform_len: usize) -> Self { + Self { + deform_len, + stack: HashMap::new(), + } + } + + /// Reset the stack. Ready to receive deformations for one frame. + pub(crate) fn reset(&mut self) { + for enabled_deform in self.stack.values_mut() { + enabled_deform.0 = false; + } + } + + /// Combine the deformations received so far according to some rules, and write to the result + pub(crate) fn combine(&self, _nodes: &InoxNodeTree, _node_comps: &World, result: &mut [Vec2]) { + if result.len() != self.deform_len { + panic!("Required output deform dimensions different from what DeformStack is initialized with.") + } + + let direct_deforms = self.stack.values().filter_map(|enabled_deform| { + if enabled_deform.0 { + let Deform::Direct(ref direct_deform) = enabled_deform.1; + Some(direct_deform) + } else { + None + } + }); + linear_combine(direct_deforms, result); + } + + /// Submit a deform from a source for a node. + pub(crate) fn push(&mut self, src: DeformSrc, mut deform: Deform) { + let Deform::Direct(ref direct_deform) = deform; + if direct_deform.len() != self.deform_len { + panic!("A direct deform with non-matching dimensions is submitted to a node."); + } + + self.stack + .entry(src) + .and_modify(|enabled_deform| { + if enabled_deform.0 { + panic!("A same source submitted deform twice for a same node within one frame.") + } + enabled_deform.0 = true; + + swap(&mut enabled_deform.1, &mut deform); + }) + .or_insert((true, deform)); + } +} diff --git a/inox2d/src/node/drawables.rs b/inox2d/src/node/drawables.rs new file mode 100644 index 0000000..5b97b19 --- /dev/null +++ b/inox2d/src/node/drawables.rs @@ -0,0 +1,62 @@ +use crate::math::transform::Transform; +use crate::node::{ + components::{Composite, Drawable, TexturedMesh}, + InoxNodeUuid, +}; +use crate::puppet::World; + +/// Possible component combinations of a renderable node. +/// +/// Future spec extensions go here. +/// For user-defined custom nodes that can be rendered, as long as a subset of their components matches one of these variants, +/// they will be picked up and enter the regular rendering pipeline. +pub(crate) enum DrawableKind<'comps> { + TexturedMesh(TexturedMeshComponents<'comps>), + Composite(CompositeComponents<'comps>), +} + +/// Pack of components for a TexturedMesh. "Part" in Inochi2D terms. +pub struct TexturedMeshComponents<'comps> { + pub transform: &'comps Transform, + pub drawable: &'comps Drawable, + pub data: &'comps TexturedMesh, +} + +/// Pack of components for a Composite node. +pub struct CompositeComponents<'comps> { + pub transform: &'comps Transform, + pub drawable: &'comps Drawable, + pub data: &'comps Composite, +} + +impl<'comps> DrawableKind<'comps> { + /// Tries to construct a renderable node data pack from the World of components: + /// - `None` if node not renderable. + /// - Panicks if component combinations invalid. + pub(crate) fn new(id: InoxNodeUuid, comps: &'comps World) -> Option { + let drawable = match comps.get::(id) { + Some(drawable) => drawable, + None => return None, + }; + let transform = comps + .get::(id) + .expect("A drawble must have associated Transform."); + let textured_mesh = comps.get::(id); + let composite = comps.get::(id); + + match (textured_mesh.is_some(), composite.is_some()) { + (true, true) => panic!("The drawable has both TexturedMesh and Composite."), + (false, false) => panic!("The drawable has neither TexturedMesh nor Composite."), + (true, false) => Some(DrawableKind::TexturedMesh(TexturedMeshComponents { + transform, + drawable, + data: textured_mesh.unwrap(), + })), + (false, true) => Some(DrawableKind::Composite(CompositeComponents { + transform, + drawable, + data: composite.unwrap(), + })), + } + } +} diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 604e849..a755d00 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -1,8 +1,14 @@ use glam::{vec2, Vec2}; -use crate::math::interp::{bi_interpolate_f32, bi_interpolate_vec2s_additive, InterpRange, InterpolateMode}; -use crate::math::matrix::Matrix2d; -use crate::node::InoxNodeUuid; +use crate::math::{ + deform::Deform, + interp::{bi_interpolate_f32, bi_interpolate_vec2s_additive, InterpRange, InterpolateMode}, + matrix::Matrix2d, +}; +use crate::node::{ + components::{deform_stack::DeformSrc, DeformStack, TexturedMesh}, + InoxNodeUuid, +}; use crate::puppet::Puppet; // use crate::render::{NodeRenderCtxs, PartRenderCtx, RenderCtxKind}; @@ -60,9 +66,13 @@ pub struct Param { pub bindings: Vec, } -/* impl Param { - pub fn apply(&self, val: Vec2, node_render_ctxs: &mut NodeRenderCtxs, deform_buf: &mut [Vec2]) { + /// Internal function that modifies puppet data according to one param set. + /// Must be only called ONCE per frame to ensure correct behavior. + /// + /// End users may repeatedly apply a same parameter for multiple times in between frames, + /// but other facilities should be present to make sure this `apply()` is only called once per parameter. + pub(crate) fn apply(&self, val: Vec2, puppet: &mut Puppet) { let val = val.clamp(self.min, self.max); let val_normed = (val - self.min) / (self.max - self.min); @@ -89,7 +99,7 @@ impl Param { // Apply offset on each binding for binding in &self.bindings { - let node_offsets = node_render_ctxs.get_mut(&binding.node).unwrap(); + let node_offsets = puppet.nodes.get_node_mut(binding.node).unwrap(); let range_in = InterpRange::new( vec2(self.axis_points.x[x_mindex], self.axis_points.y[y_mindex]), @@ -97,9 +107,11 @@ impl Param { ); match binding.values { - BindingValues::ZSort(_) => { - // Seems complicated to do currently... - // Do nothing for now + BindingValues::ZSort(ref matrix) => { + let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); + + node_offsets.zsort += + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformTX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); @@ -153,28 +165,41 @@ impl Param { matrix[(x_maxdex, y_maxdex)].as_slice(), ); - if let RenderCtxKind::Part(PartRenderCtx { - vert_offset, vert_len, .. - }) = node_offsets.kind - { - let def_beg = vert_offset as usize; - let def_end = def_beg + vert_len; - - bi_interpolate_vec2s_additive( - val_normed, - range_in, - out_top, - out_bottom, - binding.interpolate_mode, - &mut deform_buf[def_beg..def_end], - ); - } + // deform specified by a parameter must be direct, i.e., in the form of displacements of all vertices + let direct_deform = { + let textured_mesh = puppet.node_comps.get::(binding.node); + if let Some(textured_mesh) = textured_mesh { + let vert_len = textured_mesh.mesh.vertices.len(); + let mut direct_deform: Vec = Vec::with_capacity(vert_len); + direct_deform.resize(vert_len, Vec2::ZERO); + + bi_interpolate_vec2s_additive( + val_normed, + range_in, + out_top, + out_bottom, + binding.interpolate_mode, + &mut direct_deform, + ); + + direct_deform + } else { + todo!("Deform on node types other than Part.") + } + }; + + puppet + .node_comps + .get_mut::(binding.node) + .expect("Nodes being deformed must have a DeformStack component.") + .push(DeformSrc::Param(self.uuid), Deform::Direct(direct_deform)); } } } } } +/* impl Puppet { pub fn get_param(&self, uuid: ParamUuid) -> Option<&Param> { self.params.get(&uuid) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 27ae03d..65b82ff 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -2,73 +2,19 @@ mod vertex_buffers; use glam::Mat4; -use crate::math::transform::Transform; use crate::model::Model; use crate::node::{ components::{ drawable::{Mask, Masks}, - Composite, Drawable, TexturedMesh, + DeformStack, }, + drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, InoxNodeUuid, }; -use crate::puppet::{Puppet, World}; +use crate::puppet::{InoxNodeTree, Puppet, World}; use vertex_buffers::VertexBuffers; -/// Possible component combinations of a renderable node. -/// -/// Future extensions go here. -enum DrawableKind<'comps> { - TexturedMesh(TexturedMeshComponents<'comps>), - Composite(CompositeComponents<'comps>), -} - -/// Pack of components for a TexturedMesh. "Part" in Inochi2D terms. -pub struct TexturedMeshComponents<'comps> { - pub transform: &'comps Transform, - pub drawable: &'comps Drawable, - pub data: &'comps TexturedMesh, -} - -/// Pack of components for a Composite node. -pub struct CompositeComponents<'comps> { - pub transform: &'comps Transform, - pub drawable: &'comps Drawable, - pub data: &'comps Composite, -} - -impl<'comps> DrawableKind<'comps> { - /// Tries to construct a renderable node data pack from the World of components: - /// - `None` if node not renderable. - /// - Panicks if component combinations invalid. - fn new(id: InoxNodeUuid, comps: &'comps World) -> Option { - let drawable = match comps.get::(id) { - Some(drawable) => drawable, - None => return None, - }; - let transform = comps - .get::(id) - .expect("A drawble must have associated Transform."); - let textured_mesh = comps.get::(id); - let composite = comps.get::(id); - - match (textured_mesh.is_some(), composite.is_some()) { - (true, true) => panic!("The drawable has both TexturedMesh and Composite."), - (false, false) => panic!("The drawable has neither TexturedMesh nor Composite."), - (true, false) => Some(DrawableKind::TexturedMesh(TexturedMeshComponents { - transform, - drawable, - data: textured_mesh.unwrap(), - })), - (false, true) => Some(DrawableKind::Composite(CompositeComponents { - transform, - drawable, - data: composite.unwrap(), - })), - } - } -} - /// Additional info per node for rendering a TexturedMesh: /// - offset and length of array for mesh point coordinates /// - offset and length of array for indices of mesh points defining the mesh @@ -113,16 +59,19 @@ impl RenderCtx { match drawable_kind { DrawableKind::TexturedMesh(components) => { let (index_offset, vert_offset) = vertex_buffers.push(&components.data.mesh); + let (index_len, vert_len) = + (components.data.mesh.indices.len(), components.data.mesh.vertices.len()); comps.add( node.uuid, TexturedMeshRenderCtx { index_offset, vert_offset, - index_len: components.data.mesh.indices.len(), - vert_len: components.data.mesh.vertices.len(), + index_len, + vert_len, }, ); + comps.add(node.uuid, DeformStack::new(vert_len)); } DrawableKind::Composite { .. } => { // exclude non-drawable children @@ -156,6 +105,16 @@ impl RenderCtx { } } + /// Update zsort-ordered info inside self according to updated puppet. + pub(crate) fn update_zsort(&mut self, nodes: &InoxNodeTree, comps: &World) { + todo!() + } + + /// Update deform buffer content according to updated puppet. + pub(crate) fn update_deform(&mut self, nodes: &InoxNodeTree, comps: &World) { + todo!() + } + /// Memory layout: `[[x, y], [x, y], ...]` pub fn get_raw_verts(&self) -> &[f32] { VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.verts) From 3ffdb23e8f9b08f8249b9a4295875dd6a6989d39 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 21 Jul 2024 10:46:10 +0800 Subject: [PATCH 12/34] Components to store zsorts and transforms that are being modified across frames. so that `InoxNode` stores constant initial values. --- inox2d/src/math/transform.rs | 6 -- inox2d/src/node/components.rs | 8 +++ inox2d/src/node/components/deform_stack.rs | 2 +- inox2d/src/node/components/transform_store.rs | 9 +++ inox2d/src/node/components/zsort.rs | 13 ++++ inox2d/src/node/drawables.rs | 18 +++-- inox2d/src/params.rs | 69 ++++++++++++++----- inox2d/src/puppet.rs | 6 +- inox2d/src/puppet/transforms.rs | 26 +++---- inox2d/src/render.rs | 32 +++++---- 10 files changed, 131 insertions(+), 58 deletions(-) create mode 100644 inox2d/src/node/components/transform_store.rs create mode 100644 inox2d/src/node/components/zsort.rs diff --git a/inox2d/src/math/transform.rs b/inox2d/src/math/transform.rs index bf8be7c..51e60cb 100644 --- a/inox2d/src/math/transform.rs +++ b/inox2d/src/math/transform.rs @@ -1,11 +1,5 @@ use glam::{EulerRot, Mat4, Quat, Vec2, Vec3}; -#[derive(Default)] -/// absolute transform -pub struct Transform { - pub mat: Mat4, -} - /// relative transform #[derive(Debug, Clone, Copy)] pub struct TransformOffset { diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index da5f282..e344334 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -14,6 +14,12 @@ pub mod textured_mesh; /// Internal component solving for deforms of a node. pub(crate) mod deform_stack; +/// Internal component storing: +/// - Relative transform being determined in between frames. +/// - Absolute transform prepared from all relative transforms just before rendering. +pub(crate) mod transform_store; +/// Internal component storing zsort being determined in between frames. +pub(crate) mod zsort; pub use composite::Composite; pub use drawable::Drawable; @@ -21,3 +27,5 @@ pub use simple_physics::SimplePhysics; pub use textured_mesh::TexturedMesh; pub(crate) use deform_stack::DeformStack; +pub(crate) use transform_store::TransformStore; +pub(crate) use zsort::ZSort; diff --git a/inox2d/src/node/components/deform_stack.rs b/inox2d/src/node/components/deform_stack.rs index aaa12aa..ac011b5 100644 --- a/inox2d/src/node/components/deform_stack.rs +++ b/inox2d/src/node/components/deform_stack.rs @@ -1,5 +1,5 @@ -use core::mem::swap; use std::collections::HashMap; +use std::mem::swap; use glam::Vec2; diff --git a/inox2d/src/node/components/transform_store.rs b/inox2d/src/node/components/transform_store.rs new file mode 100644 index 0000000..38a6af7 --- /dev/null +++ b/inox2d/src/node/components/transform_store.rs @@ -0,0 +1,9 @@ +use glam::Mat4; + +use crate::math::transform::TransformOffset; + +#[derive(Default)] +pub(crate) struct TransformStore { + pub absolute: Mat4, + pub relative: TransformOffset, +} diff --git a/inox2d/src/node/components/zsort.rs b/inox2d/src/node/components/zsort.rs new file mode 100644 index 0000000..9b79cbd --- /dev/null +++ b/inox2d/src/node/components/zsort.rs @@ -0,0 +1,13 @@ +use std::ops::Deref; + +#[derive(Default)] +pub(crate) struct ZSort(pub f32); + +// so ZSort automatically gets the `.total_cmp()` of `f32` +impl Deref for ZSort { + type Target = f32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/inox2d/src/node/drawables.rs b/inox2d/src/node/drawables.rs index 5b97b19..db6bd57 100644 --- a/inox2d/src/node/drawables.rs +++ b/inox2d/src/node/drawables.rs @@ -1,6 +1,7 @@ -use crate::math::transform::Transform; +use glam::Mat4; + use crate::node::{ - components::{Composite, Drawable, TexturedMesh}, + components::{Composite, Drawable, TexturedMesh, TransformStore}, InoxNodeUuid, }; use crate::puppet::World; @@ -17,14 +18,16 @@ pub(crate) enum DrawableKind<'comps> { /// Pack of components for a TexturedMesh. "Part" in Inochi2D terms. pub struct TexturedMeshComponents<'comps> { - pub transform: &'comps Transform, + // Only the absolute part of `TransformStore` that the renderer backend may need. + pub transform: &'comps Mat4, pub drawable: &'comps Drawable, pub data: &'comps TexturedMesh, } /// Pack of components for a Composite node. pub struct CompositeComponents<'comps> { - pub transform: &'comps Transform, + // Only the absolute part of `TransformStore` that the renderer backend may need. + pub transform: &'comps Mat4, pub drawable: &'comps Drawable, pub data: &'comps Composite, } @@ -38,9 +41,10 @@ impl<'comps> DrawableKind<'comps> { Some(drawable) => drawable, None => return None, }; - let transform = comps - .get::(id) - .expect("A drawble must have associated Transform."); + let transform = &comps + .get::(id) + .expect("A drawble must have an associated transform.") + .absolute; let textured_mesh = comps.get::(id); let composite = comps.get::(id); diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index a755d00..b7b0797 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -6,7 +6,7 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{deform_stack::DeformSrc, DeformStack, TexturedMesh}, + components::{deform_stack::DeformSrc, DeformStack, TexturedMesh, TransformStore, ZSort}, InoxNodeUuid, }; use crate::puppet::Puppet; @@ -99,8 +99,6 @@ impl Param { // Apply offset on each binding for binding in &self.bindings { - let node_offsets = puppet.nodes.get_node_mut(binding.node).unwrap(); - let range_in = InterpRange::new( vec2(self.axis_points.x[x_mindex], self.axis_points.y[y_mindex]), vec2(self.axis_points.x[x_maxdex], self.axis_points.y[y_maxdex]), @@ -110,50 +108,85 @@ impl Param { BindingValues::ZSort(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.zsort += + puppet.node_comps.get_mut::(binding.node).unwrap().0 += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformTX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.translation.x += - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .translation + .x += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformTY(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.translation.y += - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .translation + .y += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformSX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.scale.x *= - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .scale + .x *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformSY(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.scale.y *= - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .scale + .y *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformRX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.rotation.x += - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .rotation + .x += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformRY(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.rotation.y += - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .rotation + .y += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformRZ(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - node_offsets.trans_offset.rotation.z += - bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + puppet + .node_comps + .get_mut::(binding.node) + .unwrap() + .relative + .rotation + .z += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::Deform(ref matrix) => { let out_top = InterpRange::new( diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index bee76a4..457ae52 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -53,11 +53,13 @@ impl Puppet { } /// Call this on a freshly loaded puppet if rendering is needed. Panicks on second call. - pub fn init_render_ctx(&mut self) { + pub fn init_rendering(&mut self) { if self.render_ctx.is_some() { - panic!("RenderCtx already initialized."); + panic!("Puppet already initialized for rendering."); } + self.init_node_transforms(); + let render_ctx = RenderCtx::new(self); self.render_ctx = Some(render_ctx); } diff --git a/inox2d/src/puppet/transforms.rs b/inox2d/src/puppet/transforms.rs index e27fff1..2c0a9f2 100644 --- a/inox2d/src/puppet/transforms.rs +++ b/inox2d/src/puppet/transforms.rs @@ -1,24 +1,25 @@ -use crate::math::transform::Transform; +use crate::node::components::TransformStore; use super::Puppet; impl Puppet { - /// give every node a Transform component for the absolute transform, if the puppet is going to be rendered/animated - fn init_node_transforms(&mut self) { + /// Give every node a `TransformStore` component, if the puppet is going to be rendered/animated + pub(super) fn init_node_transforms(&mut self) { for node in self.nodes.iter() { - self.node_comps.add(node.uuid, Transform::default()); + self.node_comps.add(node.uuid, TransformStore::default()); } } /// Update the puppet's nodes' absolute transforms, by combining transforms /// from each node's ancestors in a pre-order traversal manner. pub(crate) fn update_trans(&mut self) { - let root_node = self.nodes.get_node(self.nodes.root_node_id).unwrap(); - + let root_trans_store = self + .node_comps + .get_mut::(self.nodes.root_node_id) + .unwrap(); // The root's absolute transform is its relative transform. - let root_trans = root_node.trans_offset.to_matrix(); - let root_trans_comp = self.node_comps.get_mut::(root_node.uuid).unwrap(); - root_trans_comp.mat = root_trans; + let root_trans = root_trans_store.relative.to_matrix(); + root_trans_store.absolute = root_trans; // Pre-order traversal, just the order to ensure that parents are accessed earlier than children // Skip the root @@ -27,11 +28,12 @@ impl Puppet { root_trans } else { let parent = self.nodes.get_parent(node.uuid); - self.node_comps.get_mut::(parent.uuid).unwrap().mat + self.node_comps.get_mut::(parent.uuid).unwrap().absolute }; - let node_trans_comp = self.node_comps.get_mut::(node.uuid).unwrap(); - node_trans_comp.mat = base_trans * node.trans_offset.to_matrix(); + let node_trans_store = self.node_comps.get_mut::(node.uuid).unwrap(); + let node_trans = node_trans_store.relative.to_matrix(); + node_trans_store.absolute = base_trans * node_trans; } } } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 65b82ff..96b2854 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,12 +1,14 @@ mod vertex_buffers; +use std::mem::swap; + use glam::Mat4; use crate::model::Model; use crate::node::{ components::{ drawable::{Mask, Masks}, - DeformStack, + DeformStack, ZSort, }, drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, InoxNodeUuid, @@ -49,12 +51,12 @@ impl RenderCtx { let comps = &mut puppet.node_comps; let mut vertex_buffers = VertexBuffers::default(); - let mut drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); + let mut drawables_count: usize = 0; for node in nodes.iter() { let drawable_kind = DrawableKind::new(node.uuid, comps); if let Some(drawable_kind) = drawable_kind { - drawable_uuid_zsort_vec.push((node.uuid, node.zsort)); + drawables_count += 1; match drawable_kind { DrawableKind::TexturedMesh(components) => { @@ -75,7 +77,7 @@ impl RenderCtx { } DrawableKind::Composite { .. } => { // exclude non-drawable children - let mut zsorted_children_list: Vec = nodes + let children_list: Vec = nodes .get_children(node.uuid) .filter_map(|n| { if DrawableKind::new(n.uuid, comps).is_some() { @@ -85,23 +87,29 @@ impl RenderCtx { } }) .collect(); - zsorted_children_list.sort_by(|a, b| { - let zsort_a = nodes.get_node(*a).unwrap().zsort; - let zsort_b = nodes.get_node(*b).unwrap().zsort; - zsort_a.total_cmp(&zsort_b).reverse() - }); - comps.add(node.uuid, CompositeRenderCtx { zsorted_children_list }); + comps.add( + node.uuid, + CompositeRenderCtx { + // sort later, before render + zsorted_children_list: children_list, + }, + ); } }; + + // although it is tempting to put actual zsort in, a correct implementation would later set zsort values before rendering anyways. + comps.add(node.uuid, ZSort::default()); } } - drawable_uuid_zsort_vec.sort_by(|a, b| a.1.total_cmp(&b.1).reverse()); + let mut root_drawables_zsorted = Vec::new(); + // similarly, populate later, before render + root_drawables_zsorted.resize(drawables_count, InoxNodeUuid(0)); Self { vertex_buffers, - root_drawables_zsorted: drawable_uuid_zsort_vec.into_iter().map(|p| p.0).collect(), + root_drawables_zsorted, } } From 537b75a8c334c24a32d3041e037e3fd84aa9a74a Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 21 Jul 2024 11:27:58 +0800 Subject: [PATCH 13/34] zsort and deform update for `RenderCtx`. --- inox2d/src/render.rs | 71 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 64 insertions(+), 7 deletions(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 96b2854..990bbd5 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -113,14 +113,71 @@ impl RenderCtx { } } - /// Update zsort-ordered info inside self according to updated puppet. - pub(crate) fn update_zsort(&mut self, nodes: &InoxNodeTree, comps: &World) { - todo!() - } + /// Update + /// - zsort-ordered info + /// - deform buffer content + /// inside self, according to updated puppet. + pub(crate) fn update(&mut self, nodes: &InoxNodeTree, comps: &mut World) { + let mut drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); + + for node in nodes.iter() { + if let Some(drawable_kind) = DrawableKind::new(node.uuid, comps) { + let node_zsort = comps.get::(node.uuid).unwrap(); + drawable_uuid_zsort_vec.push((node.uuid, node_zsort.0)); + + match drawable_kind { + // for Composite, update zsorted children list + DrawableKind::Composite { .. } => { + // `swap()` usage is a trick that both: + // - returns mut borrowed comps early + // - does not involve any heap allocations + let mut zsorted_children_list = Vec::new(); + swap( + &mut zsorted_children_list, + &mut comps + .get_mut::(node.uuid) + .unwrap() + .zsorted_children_list, + ); + + zsorted_children_list.sort_by(|a, b| { + let zsort_a = comps.get::(*a).unwrap(); + let zsort_b = comps.get::(*b).unwrap(); + zsort_a.total_cmp(zsort_b).reverse() + }); + + swap( + &mut zsorted_children_list, + &mut comps + .get_mut::(node.uuid) + .unwrap() + .zsorted_children_list, + ); + } + // for TexturedMesh, obtain and write deforms into vertex_buffer + DrawableKind::TexturedMesh(..) => { + let render_ctx = comps.get::(node.uuid).unwrap(); + let vert_offset = render_ctx.vert_offset as usize; + let vert_len = render_ctx.vert_len; + let deform_stack = comps + .get::(node.uuid) + .expect("A TexturedMesh must have an associated DeformStack."); + + deform_stack.combine( + nodes, + comps, + &mut self.vertex_buffers.deforms[vert_offset..(vert_offset + vert_len)], + ); + } + } + } + } - /// Update deform buffer content according to updated puppet. - pub(crate) fn update_deform(&mut self, nodes: &InoxNodeTree, comps: &World) { - todo!() + drawable_uuid_zsort_vec.sort_by(|a, b| a.1.total_cmp(&b.1).reverse()); + self.root_drawables_zsorted + .iter_mut() + .zip(drawable_uuid_zsort_vec.iter()) + .for_each(|(old, new)| *old = new.0); } /// Memory layout: `[[x, y], [x, y], ...]` From 49111a78da575c9cd94cd9c7ab9c3462b1e223ec Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Fri, 26 Jul 2024 22:15:54 +0800 Subject: [PATCH 14/34] `Param` init and set. The same reasoning behind `ctx` structs: Any data not in a model file is additional context data for certain functionalities, which should be only initialized upon demand. End users only reset and set the values of params. When and how to actually apply params per frame is up to this library. Removed `physics_ctx` placeholder that is present on local but should not be published. --- inox2d/src/params.rs | 148 ++++++++++++++++++------------------------- inox2d/src/puppet.rs | 28 ++++---- 2 files changed, 78 insertions(+), 98 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index b7b0797..5cb7469 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use glam::{vec2, Vec2}; use crate::math::{ @@ -9,8 +11,7 @@ use crate::node::{ components::{deform_stack::DeformSrc, DeformStack, TexturedMesh, TransformStore, ZSort}, InoxNodeUuid, }; -use crate::puppet::Puppet; -// use crate::render::{NodeRenderCtxs, PartRenderCtx, RenderCtxKind}; +use crate::puppet::{Puppet, World}; /// Parameter binding to a node. This allows to animate a node based on the value of the parameter that owns it. pub struct Binding { @@ -67,12 +68,12 @@ pub struct Param { } impl Param { - /// Internal function that modifies puppet data according to one param set. + /// Internal function that modifies puppet components according to one param set. /// Must be only called ONCE per frame to ensure correct behavior. /// /// End users may repeatedly apply a same parameter for multiple times in between frames, /// but other facilities should be present to make sure this `apply()` is only called once per parameter. - pub(crate) fn apply(&self, val: Vec2, puppet: &mut Puppet) { + pub(crate) fn apply(&self, val: Vec2, comps: &mut World) { let val = val.clamp(self.min, self.max); let val_normed = (val - self.min) / (self.max - self.min); @@ -108,14 +109,13 @@ impl Param { BindingValues::ZSort(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet.node_comps.get_mut::(binding.node).unwrap().0 += + comps.get_mut::(binding.node).unwrap().0 += bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformTX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps + comps .get_mut::(binding.node) .unwrap() .relative @@ -125,8 +125,7 @@ impl Param { BindingValues::TransformTY(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps + comps .get_mut::(binding.node) .unwrap() .relative @@ -136,30 +135,19 @@ impl Param { BindingValues::TransformSX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps - .get_mut::(binding.node) - .unwrap() - .relative - .scale - .x *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + comps.get_mut::(binding.node).unwrap().relative.scale.x *= + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformSY(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps - .get_mut::(binding.node) - .unwrap() - .relative - .scale - .y *= bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); + comps.get_mut::(binding.node).unwrap().relative.scale.y *= + bi_interpolate_f32(val_normed, range_in, out_top, out_bottom, binding.interpolate_mode); } BindingValues::TransformRX(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps + comps .get_mut::(binding.node) .unwrap() .relative @@ -169,8 +157,7 @@ impl Param { BindingValues::TransformRY(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps + comps .get_mut::(binding.node) .unwrap() .relative @@ -180,8 +167,7 @@ impl Param { BindingValues::TransformRZ(ref matrix) => { let (out_top, out_bottom) = ranges_out(matrix, x_mindex, x_maxdex, y_mindex, y_maxdex); - puppet - .node_comps + comps .get_mut::(binding.node) .unwrap() .relative @@ -200,7 +186,7 @@ impl Param { // deform specified by a parameter must be direct, i.e., in the form of displacements of all vertices let direct_deform = { - let textured_mesh = puppet.node_comps.get::(binding.node); + let textured_mesh = comps.get::(binding.node); if let Some(textured_mesh) = textured_mesh { let vert_len = textured_mesh.mesh.vertices.len(); let mut direct_deform: Vec = Vec::with_capacity(vert_len); @@ -221,8 +207,7 @@ impl Param { } }; - puppet - .node_comps + comps .get_mut::(binding.node) .expect("Nodes being deformed must have a DeformStack component.") .push(DeformSrc::Param(self.uuid), Deform::Direct(direct_deform)); @@ -232,39 +217,60 @@ impl Param { } } -/* -impl Puppet { - pub fn get_param(&self, uuid: ParamUuid) -> Option<&Param> { - self.params.get(&uuid) - } - - pub fn get_param_mut(&mut self, uuid: ParamUuid) -> Option<&mut Param> { - self.params.get_mut(&uuid) - } - - pub fn get_named_param(&self, name: &str) -> Option<&Param> { - self.params.get(self.param_names.get(name)?) - } +pub(crate) struct ParamCtx { + values: HashMap, +} - pub fn get_named_param_mut(&mut self, name: &str) -> Option<&mut Param> { - self.params.get_mut(self.param_names.get(name)?) +impl ParamCtx { + pub fn new(puppet: &Puppet) -> Self { + Self { + values: puppet.params.iter().map(|p| (p.0.to_owned(), p.1.defaults)).collect(), + } } +} - pub fn begin_set_params(&mut self) { - // Reset all transform and deform offsets before applying bindings - for (key, value) in self.render_ctx.node_render_ctxs.iter_mut() { - value.trans_offset = self.nodes.get_node(*key).expect("node to be in tree").trans_offset; +impl Puppet { + /// Reset all params to default value. + pub fn reset_params(&mut self) { + for (name, value) in self + .param_ctx + .as_mut() + .expect("Params can be written after .init_params().") + .values + .iter_mut() + { + *value = self.params.get(name).unwrap().defaults; } + } - for v in self.render_ctx.vertex_buffers.deforms.iter_mut() { - *v = Vec2::ZERO; + /// Set param with name to value `val`. + pub fn set_param(&mut self, param_name: &str, val: Vec2) -> Result<(), SetParamError> { + if let Some(value) = self + .param_ctx + .as_mut() + .expect("Params can be written after .init_params().") + .values + .get_mut(param_name) + { + *value = val; + Ok(()) + } else { + Err(SetParamError::NoParameterNamed(param_name.to_string())) } } - pub fn end_set_params(&mut self, dt: f32) { - // TODO: find better places for these two update calls and pass elapsed time in - self.update_physics(dt, self.physics); - self.update_trans(); + /// Modify components as specified by all params. Must be called ONCE per frame. + pub(crate) fn apply_params(&mut self) { + // a correct implementation should not care about the order of `.apply()` + for (param_name, val) in self + .param_ctx + .as_mut() + .expect("Params can be applied after .init_params().") + .values + .iter() + { + self.params.get(param_name).unwrap().apply(*val, &mut self.node_comps); + } } } @@ -272,32 +278,4 @@ impl Puppet { pub enum SetParamError { #[error("No parameter named {0}")] NoParameterNamed(String), - - #[error("No parameter with uuid {0:?}")] - NoParameterWithUuid(ParamUuid), -} - -impl Puppet { - pub fn set_named_param(&mut self, param_name: &str, val: Vec2) -> Result<(), SetParamError> { - let Some(param_uuid) = self.param_names.get(param_name) else { - return Err(SetParamError::NoParameterNamed(param_name.to_string())); - }; - - self.set_param(*param_uuid, val) - } - - pub fn set_param(&mut self, param_uuid: ParamUuid, val: Vec2) -> Result<(), SetParamError> { - let Some(param) = self.params.get_mut(¶m_uuid) else { - return Err(SetParamError::NoParameterWithUuid(param_uuid)); - }; - - param.apply( - val, - &mut self.render_ctx.node_render_ctxs, - self.render_ctx.vertex_buffers.deforms.as_mut_slice(), - ); - - Ok(()) - } } -*/ diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 457ae52..adb2dfc 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -6,7 +6,7 @@ mod world; use std::collections::HashMap; use crate::node::{InoxNode, InoxNodeUuid}; -use crate::params::{Param, ParamUuid}; +use crate::params::{Param, ParamCtx}; use crate::render::RenderCtx; use meta::PuppetMeta; @@ -22,8 +22,8 @@ pub struct Puppet { pub(crate) node_comps: World, /// Context for rendering this puppet. See `.init_render_ctx()`. pub render_ctx: Option, - pub(crate) params: HashMap, - pub(crate) param_names: HashMap, + pub(crate) params: HashMap, + pub(crate) param_ctx: Option, } impl Puppet { @@ -31,24 +31,16 @@ impl Puppet { meta: PuppetMeta, physics: PuppetPhysics, root: InoxNode, - named_params: HashMap, + params: HashMap, ) -> Self { - let mut params = HashMap::new(); - let mut param_names = HashMap::new(); - for (name, param) in named_params { - param_names.insert(name, param.uuid); - params.insert(param.uuid, param); - } - Self { meta, physics, - physics_ctx: None, nodes: InoxNodeTree::new_with_root(root), node_comps: World::new(), render_ctx: None, params, - param_names, + param_ctx: None, } } @@ -63,6 +55,16 @@ impl Puppet { let render_ctx = RenderCtx::new(self); self.render_ctx = Some(render_ctx); } + + /// Call this on a puppet if params are going to be used. Panicks on second call. + pub fn init_params(&mut self) { + if self.param_ctx.is_some() { + panic!("Puppet already initialized for params."); + } + + let param_ctx = ParamCtx::new(self); + self.param_ctx = Some(param_ctx); + } } /// Global physics parameters for the puppet. From e08b1caa3891127ae2d2481656e10d89d71836b4 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 28 Jul 2024 12:34:54 +0800 Subject: [PATCH 15/34] Implement reset methods for puppet to run across frames. General pattern for a "system": Takes the form of an optional `-Ctx` struct attached to a puppet, which can be initialized if dependencies met. Interactions with the puppet data are done in methods of the struct, so a `None` system can do nothing. Current systems: Transform, Param, Render. --- inox2d/src/params.rs | 42 ++++++-------------- inox2d/src/puppet.rs | 69 +++++++++++++++++++++++++++++---- inox2d/src/puppet/transforms.rs | 40 +++++++++++-------- inox2d/src/render.rs | 15 +++++-- 4 files changed, 110 insertions(+), 56 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 5cb7469..b508ad3 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -217,41 +217,28 @@ impl Param { } } -pub(crate) struct ParamCtx { +/// Additional struct attached to a puppet for animating through params. +pub struct ParamCtx { values: HashMap, } impl ParamCtx { - pub fn new(puppet: &Puppet) -> Self { + pub(crate) fn new(puppet: &Puppet) -> Self { Self { values: puppet.params.iter().map(|p| (p.0.to_owned(), p.1.defaults)).collect(), } } -} -impl Puppet { /// Reset all params to default value. - pub fn reset_params(&mut self) { - for (name, value) in self - .param_ctx - .as_mut() - .expect("Params can be written after .init_params().") - .values - .iter_mut() - { - *value = self.params.get(name).unwrap().defaults; + pub(crate) fn reset(&mut self, params: &HashMap) { + for (name, value) in self.values.iter_mut() { + *value = params.get(name).unwrap().defaults; } } /// Set param with name to value `val`. - pub fn set_param(&mut self, param_name: &str, val: Vec2) -> Result<(), SetParamError> { - if let Some(value) = self - .param_ctx - .as_mut() - .expect("Params can be written after .init_params().") - .values - .get_mut(param_name) - { + pub fn set(&mut self, param_name: &str, val: Vec2) -> Result<(), SetParamError> { + if let Some(value) = self.values.get_mut(param_name) { *value = val; Ok(()) } else { @@ -260,20 +247,15 @@ impl Puppet { } /// Modify components as specified by all params. Must be called ONCE per frame. - pub(crate) fn apply_params(&mut self) { + pub(crate) fn apply(&self, params: &HashMap, comps: &mut World) { // a correct implementation should not care about the order of `.apply()` - for (param_name, val) in self - .param_ctx - .as_mut() - .expect("Params can be applied after .init_params().") - .values - .iter() - { - self.params.get(param_name).unwrap().apply(*val, &mut self.node_comps); + for (param_name, val) in self.values.iter() { + params.get(param_name).unwrap().apply(*val, comps); } } } +/// Possible errors setting a param. #[derive(Debug, thiserror::Error)] pub enum SetParamError { #[error("No parameter named {0}")] diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index adb2dfc..f3ee93a 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -10,6 +10,7 @@ use crate::params::{Param, ParamCtx}; use crate::render::RenderCtx; use meta::PuppetMeta; +use transforms::TransformCtx; pub use tree::InoxNodeTree; pub use world::World; @@ -17,13 +18,16 @@ pub use world::World; pub struct Puppet { pub meta: PuppetMeta, physics: PuppetPhysics, - // TODO: define the actual ctx + // TODO: define the actual ctx for physics pub(crate) nodes: InoxNodeTree, pub(crate) node_comps: World, - /// Context for rendering this puppet. See `.init_render_ctx()`. + /// Currently only a marker for if transform/zsort components are initialized. + pub(crate) transform_ctx: Option, + /// Context for rendering this puppet. See `.init_rendering()`. pub render_ctx: Option, pub(crate) params: HashMap, - pub(crate) param_ctx: Option, + /// Context for animating puppet with parameters. See `.init_params()` + pub param_ctx: Option, } impl Puppet { @@ -38,26 +42,45 @@ impl Puppet { physics, nodes: InoxNodeTree::new_with_root(root), node_comps: World::new(), + transform_ctx: None, render_ctx: None, params, param_ctx: None, } } - /// Call this on a freshly loaded puppet if rendering is needed. Panicks on second call. + /// Create a copy of node transform/zsort for modification. Panicks on second call. + pub fn init_transforms(&mut self) { + if self.transform_ctx.is_some() { + panic!("Puppet transforms already initialized.") + } + + let transform_ctx = TransformCtx::new(self); + self.transform_ctx = Some(transform_ctx); + } + + /// Call this on a freshly loaded puppet if rendering is needed. Panicks: + /// - if transforms are not initialized. + /// - on second call. pub fn init_rendering(&mut self) { + if self.transform_ctx.is_none() { + panic!("Puppet rendering depends on initialized puppet transforms.") + } if self.render_ctx.is_some() { panic!("Puppet already initialized for rendering."); } - self.init_node_transforms(); - let render_ctx = RenderCtx::new(self); self.render_ctx = Some(render_ctx); } - /// Call this on a puppet if params are going to be used. Panicks on second call. + /// Call this on a puppet if params are going to be used. Panicks: + /// - if rendering is not initialized. + /// - on second call. pub fn init_params(&mut self) { + if self.render_ctx.is_none() { + panic!("Only a puppet initialized for rendering can be animated by params."); + } if self.param_ctx.is_some() { panic!("Puppet already initialized for params."); } @@ -65,6 +88,38 @@ impl Puppet { let param_ctx = ParamCtx::new(self); self.param_ctx = Some(param_ctx); } + + /// Prepare the puppet for a new frame. User may set params afterwards. + pub fn begin_frame(&mut self) { + if let Some(render_ctx) = self.render_ctx.as_mut() { + render_ctx.reset(&self.nodes, &mut self.node_comps); + } + + if let Some(transform_ctx) = self.transform_ctx.as_mut() { + transform_ctx.reset(&self.nodes, &mut self.node_comps); + } + + if let Some(param_ctx) = self.param_ctx.as_mut() { + param_ctx.reset(&self.params); + } + } + + /// Freeze puppet for one frame. Rendering, if initialized, may follow. + /// + /// Provide elapsed time for physics, if initialized, to run. Provide `0` for the first call. + pub fn end_frame(&mut self, _dt: f32) { + if let Some(param_ctx) = self.param_ctx.as_mut() { + param_ctx.apply(&self.params, &mut self.node_comps); + } + + if let Some(transform_ctx) = self.transform_ctx.as_mut() { + transform_ctx.update(&self.nodes, &mut self.node_comps); + } + + if let Some(render_ctx) = self.render_ctx.as_mut() { + render_ctx.update(&self.nodes, &mut self.node_comps); + } + } } /// Global physics parameters for the puppet. diff --git a/inox2d/src/puppet/transforms.rs b/inox2d/src/puppet/transforms.rs index 2c0a9f2..950d59d 100644 --- a/inox2d/src/puppet/transforms.rs +++ b/inox2d/src/puppet/transforms.rs @@ -1,37 +1,45 @@ -use crate::node::components::TransformStore; +use crate::node::components::{TransformStore, ZSort}; -use super::Puppet; +use super::{InoxNodeTree, Puppet, World}; -impl Puppet { - /// Give every node a `TransformStore` component, if the puppet is going to be rendered/animated - pub(super) fn init_node_transforms(&mut self) { - for node in self.nodes.iter() { - self.node_comps.add(node.uuid, TransformStore::default()); +pub(crate) struct TransformCtx {} + +impl TransformCtx { + /// Give every node a `TransformStore` and a `ZSort` component, if the puppet is going to be rendered/animated + pub fn new(puppet: &mut Puppet) -> Self { + for node in puppet.nodes.iter() { + puppet.node_comps.add(node.uuid, TransformStore::default()); + puppet.node_comps.add(node.uuid, ZSort::default()); + } + TransformCtx {} + } + + /// Reset all transform/zsort values to default. + pub fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { + for node in nodes.iter() { + comps.get_mut::(node.uuid).unwrap().relative = node.trans_offset; } } /// Update the puppet's nodes' absolute transforms, by combining transforms /// from each node's ancestors in a pre-order traversal manner. - pub(crate) fn update_trans(&mut self) { - let root_trans_store = self - .node_comps - .get_mut::(self.nodes.root_node_id) - .unwrap(); + pub(crate) fn update(&mut self, nodes: &InoxNodeTree, comps: &mut World) { + let root_trans_store = comps.get_mut::(nodes.root_node_id).unwrap(); // The root's absolute transform is its relative transform. let root_trans = root_trans_store.relative.to_matrix(); root_trans_store.absolute = root_trans; // Pre-order traversal, just the order to ensure that parents are accessed earlier than children // Skip the root - for node in self.nodes.pre_order_iter().skip(1) { + for node in nodes.pre_order_iter().skip(1) { let base_trans = if node.lock_to_root { root_trans } else { - let parent = self.nodes.get_parent(node.uuid); - self.node_comps.get_mut::(parent.uuid).unwrap().absolute + let parent = nodes.get_parent(node.uuid); + comps.get_mut::(parent.uuid).unwrap().absolute }; - let node_trans_store = self.node_comps.get_mut::(node.uuid).unwrap(); + let node_trans_store = comps.get_mut::(node.uuid).unwrap(); let node_trans = node_trans_store.relative.to_matrix(); node_trans_store.absolute = base_trans * node_trans; } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 990bbd5..28f95bd 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -97,9 +97,6 @@ impl RenderCtx { ); } }; - - // although it is tempting to put actual zsort in, a correct implementation would later set zsort values before rendering anyways. - comps.add(node.uuid, ZSort::default()); } } @@ -113,6 +110,18 @@ impl RenderCtx { } } + /// Reset all `DeformStack`. + pub(crate) fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { + for node in nodes.iter() { + if let Some(DrawableKind::TexturedMesh(..)) = DrawableKind::new(node.uuid, comps) { + let deform_stack = comps + .get_mut::(node.uuid) + .expect("A TexturedMesh must have an associated DeformStack."); + deform_stack.reset(); + } + } + } + /// Update /// - zsort-ordered info /// - deform buffer content From 54e10987ffafd660d1ce224437968364b4934bed Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 29 Jul 2024 23:36:29 +0800 Subject: [PATCH 16/34] `render-opengl` compiles. With parameters and ECS system in place. Changes to `trait InoxRenderer` during implementation: - `new()` can have custom params. - `on_begin_draw()` and `on_end_draw()` Make `vao` creation, binding and unbinding, and vbo attaching, more explicit. Move previous OpenGL `InoxRenderer` implementation into new framework. Remove duplicate shader bind and uniform uploads during iteration over masks for a node (do once inside `on_begin_masks()`). --- examples/render-opengl/src/main.rs | 24 +- inox2d-opengl/src/gl_buffer.rs | 110 ++++---- inox2d-opengl/src/lib.rs | 345 +++++++++++++------------ inox2d/src/node/components/drawable.rs | 2 +- inox2d/src/render.rs | 21 +- 5 files changed, 250 insertions(+), 252 deletions(-) diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index f8b8282..6e4de02 100644 --- a/examples/render-opengl/src/main.rs +++ b/examples/render-opengl/src/main.rs @@ -41,12 +41,17 @@ fn main() -> Result<(), Box> { tracing::info!("Parsing puppet"); let data = fs::read(cli.inp_path)?; - let model = parse_inp(data.as_slice())?; + let mut model = parse_inp(data.as_slice())?; tracing::info!( "Successfully parsed puppet: {}", (model.puppet.meta.name.as_deref()).unwrap_or("") ); + tracing::info!("Setting up puppet for transforms, params and rendering."); + model.puppet.init_transforms(); + model.puppet.init_rendering(); + model.puppet.init_params(); + tracing::info!("Setting up windowing and OpenGL"); let app_frame = AppFrame::init( WindowBuilder::new() @@ -81,10 +86,9 @@ impl Inox2dOpenglExampleApp { impl App for Inox2dOpenglExampleApp { fn resume_window(&mut self, gl: glow::Context) { - match OpenglRenderer::new(gl) { + match OpenglRenderer::new(gl, &self.model) { Ok(mut renderer) => { tracing::info!("Initializing Inox2D renderer"); - renderer.prepare(&self.model).unwrap(); renderer.resize(self.width, self.height); renderer.camera.scale = Vec2::splat(0.15); tracing::info!("Inox2D renderer initialized"); @@ -119,12 +123,16 @@ impl App for Inox2dOpenglExampleApp { renderer.clear(); let puppet = &mut self.model.puppet; - puppet.begin_set_params(); + puppet.begin_frame(); let t = scene_ctrl.current_elapsed(); - let _ = puppet.set_named_param("Head:: Yaw-Pitch", Vec2::new(t.cos(), t.sin())); - puppet.end_set_params(scene_ctrl.dt()); - - renderer.render(puppet); + let _ = puppet + .param_ctx + .as_mut() + .unwrap() + .set("Head:: Yaw-Pitch", Vec2::new(t.cos(), t.sin())); + puppet.end_frame(scene_ctrl.dt()); + + inox2d::render::draw(renderer, puppet); } fn handle_window_event(&mut self, event: WindowEvent, elwt: &EventLoopWindowTarget<()>) { diff --git a/inox2d-opengl/src/gl_buffer.rs b/inox2d-opengl/src/gl_buffer.rs index a21d346..0e580fe 100644 --- a/inox2d-opengl/src/gl_buffer.rs +++ b/inox2d-opengl/src/gl_buffer.rs @@ -1,7 +1,5 @@ use glow::HasContext; -use inox2d::render::RenderCtx; - use super::OpenglRendererError; unsafe fn upload_array_to_gl(gl: &glow::Context, array: &[T], target: u32, usage: u32) { @@ -11,71 +9,51 @@ unsafe fn upload_array_to_gl(gl: &glow::Context, array: &[T], target: u32, us gl.buffer_data_u8_slice(target, bytes, usage); } -unsafe fn reupload_array_to_gl(gl: &glow::Context, array: &[T], target: u32, start_idx: usize, end_idx: usize) { - let slice = &array[start_idx..end_idx]; - let bytes: &[u8] = core::slice::from_raw_parts(slice.as_ptr() as *const u8, core::mem::size_of_val(slice)); - gl.buffer_sub_data_u8_slice(target, start_idx as i32, bytes); -} - -pub trait RenderCtxOpenglExt { - unsafe fn setup_gl_buffers( - &self, - gl: &glow::Context, - vao: glow::VertexArray, - ) -> Result; - unsafe fn upload_deforms_to_gl(&self, gl: &glow::Context); +/// Create a vertex array. Initialize vertex, uv, deform and index buffers, upload content and attach them to the vertex array. Return the array. +/// +/// # Errors +/// +/// This function will return an error if it couldn't create a vertex array. +/// +/// # Safety +/// +/// No prerequisites. +pub unsafe fn setup_gl_buffers( + gl: &glow::Context, + verts: &[f32], + uvs: &[f32], + deforms: &[f32], + indices: &[u16], +) -> Result { + let vao = gl.create_vertex_array().map_err(OpenglRendererError::Opengl)?; + gl.bind_vertex_array(Some(vao)); + + upload_array_to_gl(gl, verts, glow::ARRAY_BUFFER, glow::STATIC_DRAW); + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(0); + + upload_array_to_gl(gl, uvs, glow::ARRAY_BUFFER, glow::STATIC_DRAW); + gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(1); + + upload_array_to_gl(gl, deforms, glow::ARRAY_BUFFER, glow::DYNAMIC_DRAW); + gl.vertex_attrib_pointer_f32(2, 2, glow::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(2); + + upload_array_to_gl(gl, indices, glow::ELEMENT_ARRAY_BUFFER, glow::STATIC_DRAW); + + gl.bind_vertex_array(None); + Ok(vao) } -impl RenderCtxOpenglExt for RenderCtx { - /// Uploads the vertex and index buffers to OpenGL. - /// - /// # Errors - /// - /// This function will return an error if it couldn't create a vertex array. - /// - /// # Safety - /// - /// Only call this function once when loading a new puppet. - unsafe fn setup_gl_buffers( - &self, - gl: &glow::Context, - vao: glow::VertexArray, - ) -> Result { - gl.bind_vertex_array(Some(vao)); - - upload_array_to_gl(gl, &self.vertex_buffers.verts, glow::ARRAY_BUFFER, glow::STATIC_DRAW); - gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 0, 0); - gl.enable_vertex_attrib_array(0); - - upload_array_to_gl(gl, &self.vertex_buffers.uvs, glow::ARRAY_BUFFER, glow::STATIC_DRAW); - gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 0, 0); - gl.enable_vertex_attrib_array(1); - - upload_array_to_gl(gl, &self.vertex_buffers.deforms, glow::ARRAY_BUFFER, glow::DYNAMIC_DRAW); - gl.vertex_attrib_pointer_f32(2, 2, glow::FLOAT, false, 0, 0); - gl.enable_vertex_attrib_array(2); - - upload_array_to_gl( - gl, - &self.vertex_buffers.indices, - glow::ELEMENT_ARRAY_BUFFER, - glow::STATIC_DRAW, - ); - - Ok(vao) - } +/// Upload full deform buffer content. +/// +/// # Safety +/// +/// The vertex array object created in `setup_gl_buffers()` must be bound. +pub unsafe fn upload_deforms_to_gl(gl: &glow::Context, deforms: &[f32]) { + gl.enable_vertex_attrib_array(2); - /// # Safety - /// - /// unsafe as initiating GL calls. can be safely called for multiple times, - /// but only needed once after deform update and before rendering. - unsafe fn upload_deforms_to_gl(&self, gl: &glow::Context) { - reupload_array_to_gl( - gl, - &self.vertex_buffers.deforms, - glow::ARRAY_BUFFER, - 0, - self.vertex_buffers.deforms.len(), - ); - } + let bytes: &[u8] = core::slice::from_raw_parts(deforms.as_ptr() as *const u8, core::mem::size_of_val(deforms)); + gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, bytes); } diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 96ae6c6..398aafe 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -7,20 +7,28 @@ use std::cell::RefCell; use std::mem; use std::ops::Deref; -use gl_buffer::RenderCtxOpenglExt; -use glam::{uvec2, Mat4, UVec2, Vec2, Vec3}; +use glam::{uvec2, UVec2, Vec3}; use glow::HasContext; -use inox2d::texture::{decode_model_textures, TextureId}; use inox2d::math::camera::Camera; -use inox2d::model::{Model, ModelTexture}; -use inox2d::node::data::{BlendMode, Composite, Part}; +use inox2d::model::Model; +use inox2d::node::{ + components::{ + drawable::{BlendMode, Mask, MaskMode, Masks}, + TexturedMesh, + }, + drawables::{CompositeComponents, TexturedMeshComponents}, + InoxNodeUuid, +}; use inox2d::puppet::Puppet; -use inox2d::render::{InoxRenderer, InoxRendererCommon, NodeRenderCtx, PartRenderCtx}; +use inox2d::render::{CompositeRenderCtx, InoxRenderer, TexturedMeshRenderCtx}; +use inox2d::texture::{decode_model_textures, TextureId}; use self::shader::ShaderCompileError; use self::shaders::{CompositeMaskShader, CompositeShader, PartMaskShader, PartShader}; -use self::texture::{Texture, TextureError}; +use self::texture::Texture; + +use gl_buffer::{setup_gl_buffers, upload_deforms_to_gl}; #[derive(Debug, thiserror::Error)] #[error("Could not initialize OpenGL renderer: {0}")] @@ -29,8 +37,8 @@ pub enum OpenglRendererError { Opengl(String), } -#[derive(Default, Clone)] -pub struct GlCache { +#[derive(Default)] +struct GlCache { pub camera: Option, pub viewport: Option, pub blend_mode: Option, @@ -105,7 +113,6 @@ impl GlCache { } } -#[allow(unused)] pub struct OpenglRenderer { gl: glow::Context, support_debug_extension: bool, @@ -129,79 +136,7 @@ pub struct OpenglRenderer { textures: Vec, } -// TODO: remove the #[allow(unused)] -#[allow(unused)] impl OpenglRenderer { - pub fn new(gl: glow::Context) -> Result { - let vao = unsafe { gl.create_vertex_array().map_err(OpenglRendererError::Opengl)? }; - - // Initialize framebuffers - let composite_framebuffer; - let cf_albedo; - let cf_emissive; - let cf_bump; - let cf_stencil; - unsafe { - cf_albedo = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - cf_emissive = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - cf_bump = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - cf_stencil = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - - composite_framebuffer = gl.create_framebuffer().map_err(OpenglRendererError::Opengl)?; - } - - // Shaders - let part_shader = PartShader::new(&gl)?; - let part_mask_shader = PartMaskShader::new(&gl)?; - let composite_shader = CompositeShader::new(&gl)?; - let composite_mask_shader = CompositeMaskShader::new(&gl)?; - - let support_debug_extension = gl.supported_extensions().contains("GL_KHR_debug"); - - let renderer = Self { - gl, - support_debug_extension, - camera: Camera::default(), - viewport: UVec2::default(), - cache: RefCell::new(GlCache::default()), - - vao, - - composite_framebuffer, - cf_albedo, - cf_emissive, - cf_bump, - cf_stencil, - - part_shader, - part_mask_shader, - composite_shader, - composite_mask_shader, - - textures: Vec::new(), - }; - - // Set emission strength once (it doesn't change anywhere else) - renderer.bind_shader(&renderer.part_shader); - renderer.part_shader.set_emission_strength(&renderer.gl, 1.); - - Ok(renderer) - } - - fn upload_model_textures(&mut self, model_textures: &[ModelTexture]) -> Result<(), TextureError> { - // decode textures in parallel - let shalltexs = decode_model_textures(model_textures.iter()); - - // upload textures - for (i, shalltex) in shalltexs.iter().enumerate() { - tracing::debug!("Uploading shallow texture {:?}", i); - let tex = texture::Texture::from_shallow_texture(&self.gl, shalltex)?; - self.textures.push(tex); - } - - Ok(()) - } - /// Pushes an OpenGL debug group. /// This is very useful to debug OpenGL calls per node with `apitrace`, as it will nest calls inside of labels, /// making it trivial to know which calls correspond to which nodes. @@ -249,7 +184,7 @@ impl OpenglRenderer { } /// Set blending mode. See `BlendMode` for supported blend modes. - pub fn set_blend_mode(&self, blend_mode: BlendMode) { + fn set_blend_mode(&self, blend_mode: BlendMode) { if !self.cache.borrow_mut().update_blend_mode(blend_mode) { return; } @@ -298,7 +233,7 @@ impl OpenglRenderer { unsafe { self.gl.use_program(Some(program)) }; } - fn bind_part_textures(&self, part: &Part) { + fn bind_part_textures(&self, part: &TexturedMesh) { if !self.cache.borrow_mut().update_albedo(part.tex_albedo) { return; } @@ -311,7 +246,7 @@ impl OpenglRenderer { /// Clear the texture cache /// This one method missing made me pull my hair out for an entire month. - pub fn clear_texture_cache(&self) { + fn clear_texture_cache(&self) { self.cache.borrow_mut().albedo = None; } @@ -350,21 +285,8 @@ impl OpenglRenderer { gl.bind_framebuffer(glow::FRAMEBUFFER, None); } -} -impl InoxRenderer for OpenglRenderer { - type Error = OpenglRendererError; - - fn prepare(&mut self, model: &Model) -> Result<(), Self::Error> { - unsafe { model.puppet.render_ctx.setup_gl_buffers(&self.gl, self.vao)? }; - - match self.upload_model_textures(&model.textures) { - Ok(_) => Ok(()), - Err(_) => Err(OpenglRendererError::Opengl("Texture Upload Error.".to_string())), - } - } - - fn resize(&mut self, w: u32, h: u32) { + pub fn resize(&mut self, w: u32, h: u32) { self.viewport = uvec2(w, h); let gl = &self.gl; @@ -394,62 +316,119 @@ impl InoxRenderer for OpenglRenderer { self.update_camera(); } - fn clear(&self) { + pub fn clear(&self) { unsafe { self.gl.clear(glow::COLOR_BUFFER_BIT); } } +} - /* - These functions should be reworked together: - setup_gl_buffers -> should set up in a way so that the draw functions draws into a texture - on_begin/end_scene -> prepares and ends drawing to texture. also post-processing - draw_scene -> actually makes things appear on a surface - */ - - fn on_begin_scene(&self) { - todo!() - } +impl InoxRenderer for OpenglRenderer { + type NewParams = glow::Context; + type Error = OpenglRendererError; - fn render(&self, puppet: &Puppet) { - let gl = &self.gl; + fn new(gl: Self::NewParams, model: &Model) -> Result { + // Initialize framebuffers + let composite_framebuffer; + let cf_albedo; + let cf_emissive; + let cf_bump; + let cf_stencil; unsafe { - puppet.render_ctx.upload_deforms_to_gl(gl); - gl.enable(glow::BLEND); - gl.disable(glow::DEPTH_TEST); + cf_albedo = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + cf_emissive = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + cf_bump = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + cf_stencil = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + + composite_framebuffer = gl.create_framebuffer().map_err(OpenglRendererError::Opengl)?; } - let camera = self - .camera - .matrix(Vec2::new(self.viewport.x as f32, self.viewport.y as f32)); - self.draw(&camera, puppet); - } + // Shaders + let part_shader = PartShader::new(&gl)?; + let part_mask_shader = PartMaskShader::new(&gl)?; + let composite_shader = CompositeShader::new(&gl)?; + let composite_mask_shader = CompositeMaskShader::new(&gl)?; - fn on_end_scene(&self) { - todo!() - } + let support_debug_extension = gl.supported_extensions().contains("GL_KHR_debug"); + + let inox_buffers = model + .puppet + .render_ctx + .as_ref() + .expect("Rendering for a puppet must be initialized before creating a renderer."); + let vao = unsafe { + setup_gl_buffers( + &gl, + inox_buffers.get_raw_verts(), + inox_buffers.get_raw_uvs(), + inox_buffers.get_raw_deforms(), + inox_buffers.get_raw_indices(), + ) + }?; + + // decode textures in parallel + let shalltexs = decode_model_textures(model.textures.iter()); + let textures = shalltexs + .iter() + .enumerate() + .map(|e| { + tracing::debug!("Uploading shallow texture {:?}", e.0); + texture::Texture::from_shallow_texture(&gl, e.1).map_err(|e| OpenglRendererError::Opengl(e.to_string())) + }) + .collect::, _>>()?; + + let renderer = Self { + gl, + support_debug_extension, + camera: Camera::default(), + viewport: UVec2::default(), + cache: RefCell::new(GlCache::default()), + + vao, + + composite_framebuffer, + cf_albedo, + cf_emissive, + cf_bump, + cf_stencil, + + part_shader, + part_mask_shader, + composite_shader, + composite_mask_shader, + + textures, + }; + + // Set emission strength once (it doesn't change anywhere else) + renderer.bind_shader(&renderer.part_shader); + renderer.part_shader.set_emission_strength(&renderer.gl, 1.); - fn draw_scene(&self) { - todo!() + Ok(renderer) } - fn on_begin_mask(&self, has_mask: bool) { + fn on_begin_masks(&self, masks: &Masks) { let gl = &self.gl; + unsafe { gl.enable(glow::STENCIL_TEST); - gl.clear_stencil(!has_mask as i32); + gl.clear_stencil(!masks.has_masks() as i32); gl.clear(glow::STENCIL_BUFFER_BIT); gl.color_mask(false, false, false, false); gl.stencil_op(glow::KEEP, glow::KEEP, glow::REPLACE); gl.stencil_mask(0xff); } + + let part_mask_shader = &self.part_mask_shader; + self.bind_shader(part_mask_shader); + part_mask_shader.set_threshold(gl, masks.threshold.clamp(0.0, 1.0)); } - fn set_mask_mode(&self, dodge: bool) { + fn on_begin_mask(&self, mask: &Mask) { let gl = &self.gl; unsafe { - gl.stencil_func(glow::ALWAYS, !dodge as i32, 0xff); + gl.stencil_func(glow::ALWAYS, (mask.mode == MaskMode::Mask) as i32, 0xff); } } @@ -472,9 +451,16 @@ impl InoxRenderer for OpenglRenderer { } } - fn draw_mesh_self(&self, _as_mask: bool, _camera: &Mat4) { - // TODO + fn draw_textured_mesh_content( + &self, + as_mask: bool, + components: &TexturedMeshComponents, + render_ctx: &TexturedMeshRenderCtx, + _id: InoxNodeUuid, + ) { + let gl = &self.gl; + // TODO: plain masks, meshes as masks without textures /* maskShader.use(); maskShader.setUniform(offset, data.origin); @@ -491,36 +477,19 @@ impl InoxRenderer for OpenglRenderer { // Disable the vertex attribs after use glDisableVertexAttribArray(0); */ - todo!() - } - fn draw_part_self( - &self, - as_mask: bool, - camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, - ) { - let gl = &self.gl; - - self.bind_part_textures(part); - self.set_blend_mode(part.draw_state.blend_mode); + self.bind_part_textures(components.data); + self.set_blend_mode(components.drawable.blending.mode); - let part_shader = &self.part_shader; - self.bind_shader(part_shader); - // vert uniforms - let mvp = *camera * node_render_ctx.trans; + let mvp = self.camera.matrix(self.viewport.as_vec2()) * *components.transform; if as_mask { - let part_mask_shader = &self.part_mask_shader; - self.bind_shader(part_mask_shader); + // if as_mask is set, in .on_begin_masks(): + // - part_mask_shader must have been bound and prepared. + // - mask threshold must have been uploaded. // vert uniforms - part_mask_shader.set_mvp(gl, mvp); - - // frag uniforms - part_mask_shader.set_threshold(gl, part.draw_state.mask_threshold.clamp(0.0, 1.0)); + self.part_mask_shader.set_mvp(gl, mvp); } else { let part_shader = &self.part_shader; self.bind_shader(part_shader); @@ -529,23 +498,28 @@ impl InoxRenderer for OpenglRenderer { part_shader.set_mvp(gl, mvp); // frag uniforms - part_shader.set_opacity(gl, part.draw_state.opacity); - part_shader.set_mult_color(gl, part.draw_state.tint); - part_shader.set_screen_color(gl, part.draw_state.screen_tint); + part_shader.set_opacity(gl, components.drawable.blending.opacity); + part_shader.set_mult_color(gl, components.drawable.blending.tint); + part_shader.set_screen_color(gl, components.drawable.blending.screen_tint); } unsafe { - gl.bind_vertex_array(Some(self.vao)); gl.draw_elements( glow::TRIANGLES, - part.mesh.indices.len() as i32, + render_ctx.index_len as i32, glow::UNSIGNED_SHORT, - part_render_ctx.index_offset as i32 * mem::size_of::() as i32, + render_ctx.index_offset as i32 * mem::size_of::() as i32, ); } } - fn begin_composite_content(&self) { + fn begin_composite_content( + &self, + _as_mask: bool, + _components: &CompositeComponents, + _render_ctx: &CompositeRenderCtx, + _id: InoxNodeUuid, + ) { self.clear_texture_cache(); let gl = &self.gl; @@ -566,7 +540,13 @@ impl InoxRenderer for OpenglRenderer { } } - fn finish_composite_content(&self, as_mask: bool, composite: &Composite) { + fn finish_composite_content( + &self, + as_mask: bool, + components: &CompositeComponents, + _render_ctx: &CompositeRenderCtx, + _id: InoxNodeUuid, + ) { let gl = &self.gl; self.clear_texture_cache(); @@ -574,7 +554,7 @@ impl InoxRenderer for OpenglRenderer { gl.bind_framebuffer(glow::FRAMEBUFFER, None); } - let comp = &composite.draw_state; + let blending = &components.drawable.blending; if as_mask { /* cShaderMask.use(); @@ -595,11 +575,11 @@ impl InoxRenderer for OpenglRenderer { gl.bind_texture(glow::TEXTURE_2D, Some(self.cf_bump)); } - self.set_blend_mode(comp.blend_mode); + self.set_blend_mode(blending.mode); - let opacity = comp.opacity.clamp(0.0, 1.0); - let tint = comp.tint.clamp(Vec3::ZERO, Vec3::ONE); - let screen_tint = comp.screen_tint.clamp(Vec3::ZERO, Vec3::ONE); + let opacity = blending.opacity.clamp(0.0, 1.0); + let tint = blending.tint.clamp(Vec3::ZERO, Vec3::ONE); + let screen_tint = blending.screen_tint.clamp(Vec3::ZERO, Vec3::ONE); let composite_shader = &self.composite_shader; self.bind_shader(composite_shader); @@ -612,4 +592,33 @@ impl InoxRenderer for OpenglRenderer { gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_SHORT, 0); } } + + fn on_begin_draw(&self, puppet: &Puppet) { + let gl = &self.gl; + + // TODO: calculate this matrix only once per draw pass. + // let matrix = self.camera.matrix(self.viewport.as_vec2()); + + unsafe { + gl.bind_vertex_array(Some(self.vao)); + upload_deforms_to_gl( + gl, + puppet + .render_ctx + .as_ref() + .expect("Rendering for a puppet must be initialized by now.") + .get_raw_deforms(), + ); + gl.enable(glow::BLEND); + gl.disable(glow::DEPTH_TEST); + } + } + + fn on_end_draw(&self, _puppet: &Puppet) { + let gl = &self.gl; + + unsafe { + gl.bind_vertex_array(None); + } + } } diff --git a/inox2d/src/node/components/drawable.rs b/inox2d/src/node/components/drawable.rs index ffa5861..8033c57 100644 --- a/inox2d/src/node/components/drawable.rs +++ b/inox2d/src/node/components/drawable.rs @@ -16,7 +16,7 @@ pub struct Blending { pub opacity: f32, } -#[derive(Default)] +#[derive(Default, PartialEq, Clone, Copy)] pub enum BlendMode { /// Normal blending mode. #[default] diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 28f95bd..82207ca 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -2,8 +2,6 @@ mod vertex_buffers; use std::mem::swap; -use glam::Mat4; - use crate::model::Model; use crate::node::{ components::{ @@ -15,7 +13,7 @@ use crate::node::{ }; use crate::puppet::{InoxNodeTree, Puppet, World}; -use vertex_buffers::VertexBuffers; +pub use vertex_buffers::VertexBuffers; /// Additional info per node for rendering a TexturedMesh: /// - offset and length of array for mesh point coordinates @@ -220,17 +218,13 @@ pub trait InoxRenderer where Self: Sized, { + type NewParams; type Error; /// Create a renderer for one model. /// /// Ref impl: Upload textures. - fn new(model: &Model) -> Result; - - /// Sets a quaternion that can translate, rotate and scale the whole puppet. - fn set_camera(&mut self, camera: &Mat4); - /// Returns the quaternion set by `.set_camera()`. - fn camera(&self) -> &Mat4; + fn new(params: Self::NewParams, model: &Model) -> Result; /// Begin masking. /// @@ -277,6 +271,13 @@ where render_ctx: &CompositeRenderCtx, id: InoxNodeUuid, ); + + /// Things to do before one pass of drawing a puppet. + /// + /// Ref impl: Upload deform buffer content. + fn on_begin_draw(&self, puppet: &Puppet); + /// Things to do after one pass of drawing a puppet. + fn on_end_draw(&self, puppet: &Puppet); } trait InoxRendererCommon { @@ -370,5 +371,7 @@ impl InoxRendererCommon for T { /// - The provided `InoxRender` implementation is wrong. /// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. pub fn draw(renderer: &T, puppet: &Puppet) { + renderer.on_begin_draw(puppet); renderer.draw(puppet); + renderer.on_end_draw(puppet); } From 30ecda464b82aeae47e6397ec57921873e487066 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Mon, 29 Jul 2024 23:36:55 +0800 Subject: [PATCH 17/34] Type annotations for `transmute()` by clippy. --- inox2d/src/puppet/world.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs index c14bab3..ebad444 100644 --- a/inox2d/src/puppet/world.rs +++ b/inox2d/src/puppet/world.rs @@ -31,7 +31,7 @@ impl AnyVec { Self { // SAFETY: ManuallyDrop guaranteed to have same bit layout as inner, and inner is a proper Vec // provenance considerations present, see comment for VecBytes - vec_bytes: unsafe { transmute(vec) }, + vec_bytes: unsafe { transmute::>, VecBytes>(vec) }, // SAFETY: only to be called once at end of lifetime, and vec_bytes contain a valid Vec throughout self lifetime drop: |vec_bytes| unsafe { let vec: Vec = transmute(*vec_bytes); From 6050b79bc604f5a3b5d9d53ebfb3c38d9ea7056a Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Tue, 30 Jul 2024 07:08:28 +0800 Subject: [PATCH 18/34] More comments on `upload_deforms_to_gl()`. --- inox2d-opengl/src/gl_buffer.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/inox2d-opengl/src/gl_buffer.rs b/inox2d-opengl/src/gl_buffer.rs index 0e580fe..f52968a 100644 --- a/inox2d-opengl/src/gl_buffer.rs +++ b/inox2d-opengl/src/gl_buffer.rs @@ -50,10 +50,9 @@ pub unsafe fn setup_gl_buffers( /// /// # Safety /// -/// The vertex array object created in `setup_gl_buffers()` must be bound. +/// The vertex array object created in `setup_gl_buffers()` must be bound and no new ARRAY_BUFFER is enabled. pub unsafe fn upload_deforms_to_gl(gl: &glow::Context, deforms: &[f32]) { - gl.enable_vertex_attrib_array(2); - + // if the above preconditions are met, deform is then the currently bound ARRAY_BUFFER. let bytes: &[u8] = core::slice::from_raw_parts(deforms.as_ptr() as *const u8, core::mem::size_of_val(deforms)); gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, bytes); } From fc3ab0dedf5be2d3f8fbcfa02265c2693bddde5a Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Tue, 30 Jul 2024 07:32:33 +0800 Subject: [PATCH 19/34] Fix uninitialized zsort. --- inox2d/src/puppet/transforms.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/inox2d/src/puppet/transforms.rs b/inox2d/src/puppet/transforms.rs index 950d59d..98bfafe 100644 --- a/inox2d/src/puppet/transforms.rs +++ b/inox2d/src/puppet/transforms.rs @@ -18,6 +18,7 @@ impl TransformCtx { pub fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { for node in nodes.iter() { comps.get_mut::(node.uuid).unwrap().relative = node.trans_offset; + comps.get_mut::(node.uuid).unwrap().0 = node.zsort; } } From 00725d65dbc2e61ac9654dfc6cab0743ef957a59 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Tue, 30 Jul 2024 10:51:58 +0800 Subject: [PATCH 20/34] Fix zsort, thus rendering is back for `Aka.inp`. Properly implement zsort inheritance (+). Correctly exclude composite children from `root_drawables_zsorted`. Turn off param application temporarily, so this commit works for `Aka.inp`. --- inox2d/src/node/components/transform_store.rs | 1 + inox2d/src/node/components/zsort.rs | 2 ++ inox2d/src/puppet.rs | 2 +- inox2d/src/puppet/transforms.rs | 16 ++++++++--- inox2d/src/render.rs | 27 ++++++++++++------- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/inox2d/src/node/components/transform_store.rs b/inox2d/src/node/components/transform_store.rs index 38a6af7..a3d62c2 100644 --- a/inox2d/src/node/components/transform_store.rs +++ b/inox2d/src/node/components/transform_store.rs @@ -2,6 +2,7 @@ use glam::Mat4; use crate::math::transform::TransformOffset; +/// Component holding transform values that may be modified across frames. #[derive(Default)] pub(crate) struct TransformStore { pub absolute: Mat4, diff --git a/inox2d/src/node/components/zsort.rs b/inox2d/src/node/components/zsort.rs index 9b79cbd..25e5537 100644 --- a/inox2d/src/node/components/zsort.rs +++ b/inox2d/src/node/components/zsort.rs @@ -1,5 +1,7 @@ use std::ops::Deref; +/// Component holding zsort values that may be modified across frames. +// only one value instead of absolute + relative as in TransformStore, cause inheritance of zsort (+) is commutative #[derive(Default)] pub(crate) struct ZSort(pub f32); diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index f3ee93a..bd3ce60 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -109,7 +109,7 @@ impl Puppet { /// Provide elapsed time for physics, if initialized, to run. Provide `0` for the first call. pub fn end_frame(&mut self, _dt: f32) { if let Some(param_ctx) = self.param_ctx.as_mut() { - param_ctx.apply(&self.params, &mut self.node_comps); + // param_ctx.apply(&self.params, &mut self.node_comps); } if let Some(transform_ctx) = self.transform_ctx.as_mut() { diff --git a/inox2d/src/puppet/transforms.rs b/inox2d/src/puppet/transforms.rs index 98bfafe..0c38fb6 100644 --- a/inox2d/src/puppet/transforms.rs +++ b/inox2d/src/puppet/transforms.rs @@ -30,19 +30,27 @@ impl TransformCtx { let root_trans = root_trans_store.relative.to_matrix(); root_trans_store.absolute = root_trans; + let root_zsort = comps.get_mut::(nodes.root_node_id).unwrap().0; + // Pre-order traversal, just the order to ensure that parents are accessed earlier than children // Skip the root for node in nodes.pre_order_iter().skip(1) { - let base_trans = if node.lock_to_root { - root_trans + let base = if node.lock_to_root { + (root_trans, root_zsort) } else { let parent = nodes.get_parent(node.uuid); - comps.get_mut::(parent.uuid).unwrap().absolute + ( + comps.get_mut::(parent.uuid).unwrap().absolute, + comps.get_mut::(parent.uuid).unwrap().0, + ) }; let node_trans_store = comps.get_mut::(node.uuid).unwrap(); let node_trans = node_trans_store.relative.to_matrix(); - node_trans_store.absolute = base_trans * node_trans; + node_trans_store.absolute = base.0 * node_trans; + + let node_zsort = comps.get_mut::(node.uuid).unwrap(); + node_zsort.0 += base.1; } } } diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 82207ca..1937976 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -50,11 +50,11 @@ impl RenderCtx { let mut vertex_buffers = VertexBuffers::default(); - let mut drawables_count: usize = 0; + let mut root_drawables_count: usize = 0; for node in nodes.iter() { let drawable_kind = DrawableKind::new(node.uuid, comps); if let Some(drawable_kind) = drawable_kind { - drawables_count += 1; + root_drawables_count += 1; match drawable_kind { DrawableKind::TexturedMesh(components) => { @@ -86,6 +86,9 @@ impl RenderCtx { }) .collect(); + // composite children are excluded from root_drawables_zsorted + root_drawables_count -= children_list.len(); + comps.add( node.uuid, CompositeRenderCtx { @@ -100,7 +103,7 @@ impl RenderCtx { let mut root_drawables_zsorted = Vec::new(); // similarly, populate later, before render - root_drawables_zsorted.resize(drawables_count, InoxNodeUuid(0)); + root_drawables_zsorted.resize(root_drawables_count, InoxNodeUuid(0)); Self { vertex_buffers, @@ -125,12 +128,18 @@ impl RenderCtx { /// - deform buffer content /// inside self, according to updated puppet. pub(crate) fn update(&mut self, nodes: &InoxNodeTree, comps: &mut World) { - let mut drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); + let mut root_drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); - for node in nodes.iter() { + // root is definitely not a drawable. + for node in nodes.iter().skip(1) { if let Some(drawable_kind) = DrawableKind::new(node.uuid, comps) { - let node_zsort = comps.get::(node.uuid).unwrap(); - drawable_uuid_zsort_vec.push((node.uuid, node_zsort.0)); + let parent = nodes.get_parent(node.uuid); + let node_zsort = comps.get::(node.uuid).unwrap().0; + + if !matches!(DrawableKind::new(parent.uuid, comps), Some(DrawableKind::Composite(_))) { + // exclude composite children + root_drawable_uuid_zsort_vec.push((node.uuid, node_zsort)); + } match drawable_kind { // for Composite, update zsorted children list @@ -180,10 +189,10 @@ impl RenderCtx { } } - drawable_uuid_zsort_vec.sort_by(|a, b| a.1.total_cmp(&b.1).reverse()); + root_drawable_uuid_zsort_vec.sort_by(|a, b| a.1.total_cmp(&b.1).reverse()); self.root_drawables_zsorted .iter_mut() - .zip(drawable_uuid_zsort_vec.iter()) + .zip(root_drawable_uuid_zsort_vec.iter()) .for_each(|(old, new)| *old = new.0); } From c1ade823591f9120d38399ba2c74f7879ad34bcb Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Tue, 30 Jul 2024 11:40:26 +0800 Subject: [PATCH 21/34] Params (and deforms) back online: Dirty zero check patching for an old bug. Currently, overflow will happen if value (0, 0) is applied on a param, which is clearly a bug. The actual fix is not in the scope of ECS refactor. --- inox2d/src/params.rs | 5 ++++- inox2d/src/puppet.rs | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index b508ad3..f831e8f 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -250,7 +250,10 @@ impl ParamCtx { pub(crate) fn apply(&self, params: &HashMap, comps: &mut World) { // a correct implementation should not care about the order of `.apply()` for (param_name, val) in self.values.iter() { - params.get(param_name).unwrap().apply(*val, comps); + // TODO: a correct implementation should not fail on param value (0, 0) + if *val != Vec2::ZERO { + params.get(param_name).unwrap().apply(*val, comps); + } } } } diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index bd3ce60..f3ee93a 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -109,7 +109,7 @@ impl Puppet { /// Provide elapsed time for physics, if initialized, to run. Provide `0` for the first call. pub fn end_frame(&mut self, _dt: f32) { if let Some(param_ctx) = self.param_ctx.as_mut() { - // param_ctx.apply(&self.params, &mut self.node_comps); + param_ctx.apply(&self.params, &mut self.node_comps); } if let Some(transform_ctx) = self.transform_ctx.as_mut() { From 5e03996221887f55e006186663e9c107468faab3 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Wed, 31 Jul 2024 16:02:46 +0800 Subject: [PATCH 22/34] Physics cleanup. The philosophy of the ECS refactor naturally leads to this cleanup: In this refactor, spec-defined variables either goes into `InoxNode` or a component. If there are additional variables needed for the execution of a certain module, they are attached as additional components later. It is revealed that bob position of a pendulum is such a variable. After cleanup, all pendulum-like systems can provide methods for accessing and updating their bobs' position, implementing `trait Pendulum`. And then `trait SimplePhysicsCtx` will be auto implemented for all `impl Pendulum`, handling the mapping from transforms to simulation input, from simulation input to parameter values. More comments are added. --- inox2d/src/formats/payload.rs | 3 +- inox2d/src/lib.rs | 1 + inox2d/src/node/components/simple_physics.rs | 15 +++ inox2d/src/node/components/transform_store.rs | 2 +- inox2d/src/physics.rs | 123 +++++------------- inox2d/src/physics/pendulum.rs | 71 ++++++++++ inox2d/src/physics/pendulum/rigid.rs | 50 +++---- inox2d/src/physics/pendulum/spring.rs | 64 ++++----- inox2d/src/physics/runge_kutta.rs | 101 +++++++------- inox2d/src/puppet.rs | 7 +- 10 files changed, 237 insertions(+), 200 deletions(-) diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index f398ed6..7568f22 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -9,7 +9,8 @@ use crate::math::transform::TransformOffset; use crate::node::components::{drawable::*, simple_physics::*, textured_mesh::*, *}; use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{AxisPoints, Binding, BindingValues, Param, ParamUuid}; -use crate::puppet::{meta::*, Puppet, PuppetPhysics}; +use crate::physics::PuppetPhysics; +use crate::puppet::{meta::*, Puppet}; use crate::texture::TextureId; use super::f32s_as_vec2s; diff --git a/inox2d/src/lib.rs b/inox2d/src/lib.rs index 22a1f69..987f3e7 100644 --- a/inox2d/src/lib.rs +++ b/inox2d/src/lib.rs @@ -3,6 +3,7 @@ pub mod math; pub mod model; pub mod node; pub mod params; +pub mod physics; pub mod puppet; pub mod render; pub mod texture; diff --git a/inox2d/src/node/components/simple_physics.rs b/inox2d/src/node/components/simple_physics.rs index 19337df..4f8faac 100644 --- a/inox2d/src/node/components/simple_physics.rs +++ b/inox2d/src/node/components/simple_physics.rs @@ -1,6 +1,10 @@ use glam::Vec2; use crate::params::ParamUuid; +use crate::physics::{ + pendulum::{rigid::RigidPendulum, spring::SpringPendulum}, + runge_kutta::PhysicsState, +}; /// If has this as a component, the node is capable of doing Inochi2D SimplePhysics simulations pub struct SimplePhysics { @@ -48,3 +52,14 @@ impl Default for PhysicsProps { } } } + +/// Physical states for simulating a rigid pendulum. +pub(crate) struct RigidPendulumCtx { + pub bob: Vec2, + pub state: PhysicsState<2, RigidPendulum>, +} + +/// Physical states for simulating a spring pendulum. +pub(crate) struct SpringPendulumCtx { + pub state: PhysicsState<4, SpringPendulum>, +} diff --git a/inox2d/src/node/components/transform_store.rs b/inox2d/src/node/components/transform_store.rs index a3d62c2..3579eb4 100644 --- a/inox2d/src/node/components/transform_store.rs +++ b/inox2d/src/node/components/transform_store.rs @@ -4,7 +4,7 @@ use crate::math::transform::TransformOffset; /// Component holding transform values that may be modified across frames. #[derive(Default)] -pub(crate) struct TransformStore { +pub struct TransformStore { pub absolute: Mat4, pub relative: TransformOffset, } diff --git a/inox2d/src/physics.rs b/inox2d/src/physics.rs index 40e1878..ae0e617 100644 --- a/inox2d/src/physics.rs +++ b/inox2d/src/physics.rs @@ -1,106 +1,55 @@ pub mod pendulum; pub(crate) mod runge_kutta; -use std::f32::consts::PI; +use glam::Vec2; -use glam::{vec2, vec4, Vec2}; +use crate::node::components::{SimplePhysics, TransformStore}; -use crate::node::data::{InoxData, ParamMapMode, PhysicsModel, SimplePhysics}; -use crate::puppet::{Puppet, PuppetPhysics}; -use crate::render::NodeRenderCtx; - -impl Puppet { - /// Update the puppet's nodes' absolute transforms, by applying further displacements yielded by the physics system - /// in response to displacements caused by parameter changes - pub fn update_physics(&mut self, dt: f32, puppet_physics: PuppetPhysics) { - for driver_uuid in self.drivers.clone() { - let Some(driver) = self.nodes.get_node_mut(driver_uuid) else { - continue; - }; - - let InoxData::SimplePhysics(ref mut system) = driver.data else { - continue; - }; +/// Global physics parameters for the puppet. +pub struct PuppetPhysics { + pub pixels_per_meter: f32, + pub gravity: f32, +} - let nrc = &self.render_ctx.node_render_ctxs[&driver.uuid]; +type SimplePhysicsProps<'a> = (&'a PuppetPhysics, &'a SimplePhysics); + +/// Components implementing this will be able to yield a parameter value every frame based on +/// - time history of transforms of the associated node. +/// - Physics simulation. +pub trait SimplePhysicsCtx { + /// Type of input to the simulation. + type Anchor; + + /// Convert node transform to input. + fn calc_anchor(&self, props: &SimplePhysicsProps, transform: &TransformStore) -> Self::Anchor; + /// Run one step of simulation given input. + fn tick(&mut self, props: &SimplePhysicsProps, anchor: &Self::Anchor, t: f32, dt: f32); + /// Convert simulation result into a parameter value to set. + fn calc_output(&self, props: &SimplePhysicsProps, transform: &TransformStore, anchor: Self::Anchor) -> Vec2; +} - let output = system.update(dt, puppet_physics, nrc); - let param_uuid = system.param; - let _ = self.set_param(param_uuid, output); - } - } +/// Auto implemented trait for all `impl SimplePhysicsCtx`. +trait SimplePhysicsCtxCommon { + fn update(&mut self, props: &SimplePhysicsProps, transform: &TransformStore, t: f32, dt: f32) -> Vec2; } -impl SimplePhysics { - fn update(&mut self, dt: f32, puppet_physics: PuppetPhysics, node_render_ctx: &NodeRenderCtx) -> Vec2 { +impl SimplePhysicsCtxCommon for T { + /// Run physics simulation for one frame given provided methods. Handle big `dt` problems. + fn update(&mut self, props: &SimplePhysicsProps, transform: &TransformStore, t: f32, dt: f32) -> Vec2 { // Timestep is limited to 10 seconds. // If you're getting 0.1 FPS, you have bigger issues to deal with. let mut dt = dt.min(10.); - let anchor = self.calc_anchor(node_render_ctx); + let anchor = self.calc_anchor(props, transform); - // Minimum physics timestep: 0.01s - while dt > 0.01 { - self.tick(0.01, anchor, puppet_physics); + // Minimum physics timestep: 0.01s. If not satisfied, break simulation into steps. + let mut t = t; + while dt > 0. { + self.tick(props, &anchor, t, dt.min(0.01)); + t += 0.01; dt -= 0.01; } - self.tick(dt, anchor, puppet_physics); - - self.calc_output(anchor, node_render_ctx) - } - - fn tick(&mut self, dt: f32, anchor: Vec2, puppet_physics: PuppetPhysics) { - // enum dispatch, fill the branches once other systems are implemented - // as for inox2d, users are not expected to bring their own physics system, - // no need to do dynamic dispatch with something like Box - self.bob = match &mut self.model_type { - PhysicsModel::RigidPendulum(state) => state.tick(puppet_physics, &self.props, self.bob, anchor, dt), - PhysicsModel::SpringPendulum(state) => state.tick(puppet_physics, &self.props, self.bob, anchor, dt), - }; - } - - fn calc_anchor(&self, node_render_ctx: &NodeRenderCtx) -> Vec2 { - let anchor = match self.local_only { - true => node_render_ctx.trans_offset.translation.extend(1.0), - false => node_render_ctx.trans * vec4(0.0, 0.0, 0.0, 1.0), - }; - - vec2(anchor.x, anchor.y) - } - - fn calc_output(&self, anchor: Vec2, node_render_ctx: &NodeRenderCtx) -> Vec2 { - let oscale = self.props.output_scale; - let bob = self.bob; - - // "Okay, so this is confusing. We want to translate the angle back to local space, but not the coordinates." - // - Asahi Lina - - // Transform the physics output back into local space. - // The origin here is the anchor. This gives us the local angle. - let local_pos4 = match self.local_only { - true => vec4(bob.x, bob.y, 0.0, 1.0), - false => node_render_ctx.trans.inverse() * vec4(bob.x, bob.y, 0.0, 1.0), - }; - - let local_angle = vec2(local_pos4.x, local_pos4.y).normalize(); - - // Figure out the relative length. We can work this out directly in global space. - let relative_length = bob.distance(anchor) / self.props.length; - - let param_value = match self.map_mode { - ParamMapMode::XY => { - let local_pos_norm = local_angle * relative_length; - let mut result = local_pos_norm - Vec2::Y; - result.y = -result.y; // Y goes up for params - result - } - ParamMapMode::AngleLength => { - let a = f32::atan2(-local_angle.x, local_angle.y) / PI; - vec2(a, relative_length) - } - }; - - param_value * oscale + self.calc_output(props, transform, anchor) } } diff --git a/inox2d/src/physics/pendulum.rs b/inox2d/src/physics/pendulum.rs index 150771f..4a3b8a7 100644 --- a/inox2d/src/physics/pendulum.rs +++ b/inox2d/src/physics/pendulum.rs @@ -1,2 +1,73 @@ pub mod rigid; pub mod spring; + +use std::f32::consts::PI; + +use glam::{Vec2, Vec4}; + +use crate::node::components::{simple_physics::ParamMapMode, TransformStore}; + +use super::{SimplePhysicsCtx, SimplePhysicsProps}; + +/// All pendulum-like physical systems have a bob. +/// +/// For such systems, auto implement input and parameter mapping for `SimplePhysicsCtx`. +trait Pendulum { + fn get_bob(&self) -> Vec2; + fn set_bob(&mut self, bob: Vec2); + /// Compute new anchor position give current anchor and time. + fn tick(&mut self, props: &SimplePhysicsProps, anchor: Vec2, t: f32, dt: f32) -> Vec2; +} + +impl SimplePhysicsCtx for T { + type Anchor = Vec2; + + fn calc_anchor(&self, props: &SimplePhysicsProps, transform: &TransformStore) -> Vec2 { + let anchor = match props.1.local_only { + true => transform.relative.translation.extend(1.0), + false => transform.absolute * Vec4::new(0.0, 0.0, 0.0, 1.0), + }; + + Vec2::new(anchor.x, anchor.y) + } + + fn tick(&mut self, props: &SimplePhysicsProps, anchor: &Vec2, t: f32, dt: f32) { + let bob = Pendulum::tick(self, props, *anchor, t, dt); + self.set_bob(bob); + } + + fn calc_output(&self, props: &SimplePhysicsProps, transform: &TransformStore, anchor: Vec2) -> Vec2 { + let oscale = props.1.props.output_scale; + let bob = self.get_bob(); + + // "Okay, so this is confusing. We want to translate the angle back to local space, but not the coordinates." + // - Asahi Lina + + // Transform the physics output back into local space. + // The origin here is the anchor. This gives us the local angle. + let local_pos4 = match props.1.local_only { + true => Vec4::new(bob.x, bob.y, 0.0, 1.0), + false => transform.absolute.inverse() * Vec4::new(bob.x, bob.y, 0.0, 1.0), + }; + + let local_angle = Vec2::new(local_pos4.x, local_pos4.y).normalize(); + + // Figure out the relative length. We can work this out directly in global space. + let relative_length = bob.distance(anchor) / props.1.props.length; + + let param_value = match props.1.map_mode { + ParamMapMode::XY => { + let local_pos_norm = local_angle * relative_length; + let mut result = local_pos_norm - Vec2::Y; + result.y = -result.y; // Y goes up for params + result + } + ParamMapMode::AngleLength => { + let a = f32::atan2(-local_angle.x, local_angle.y) / PI; + Vec2::new(a, relative_length) + } + }; + + param_value * oscale + } +} diff --git a/inox2d/src/physics/pendulum/rigid.rs b/inox2d/src/physics/pendulum/rigid.rs index ec39627..ae82f4d 100644 --- a/inox2d/src/physics/pendulum/rigid.rs +++ b/inox2d/src/physics/pendulum/rigid.rs @@ -1,12 +1,14 @@ use glam::{vec2, Vec2}; -use crate::node::data::PhysicsProps; -use crate::physics::runge_kutta::{self, IsPhysicsVars, PhysicsState}; -use crate::puppet::PuppetPhysics; +use crate::node::components::simple_physics::{PhysicsProps, RigidPendulumCtx}; +use crate::physics::{ + pendulum::Pendulum, + runge_kutta::{IsPhysicsVars, PhysicsState}, + PuppetPhysics, SimplePhysicsProps, +}; -#[repr(C)] -#[derive(Debug, Clone, Copy, Default)] -pub struct RigidPendulum { +/// Variables for Runge-Kutta method. +pub(crate) struct RigidPendulum { pub θ: f32, pub ω: f32, } @@ -21,34 +23,36 @@ impl IsPhysicsVars<2> for RigidPendulum { } } -impl PhysicsState { - pub(crate) fn tick( - &mut self, - puppet_physics: PuppetPhysics, - props: &PhysicsProps, - bob: Vec2, - anchor: Vec2, - dt: f32, - ) -> Vec2 { +impl Pendulum for RigidPendulumCtx { + fn get_bob(&self) -> Vec2 { + self.bob + } + + fn set_bob(&mut self, bob: Vec2) { + self.bob = bob; + } + + fn tick(&mut self, props: &SimplePhysicsProps, anchor: Vec2, t: f32, dt: f32) -> Vec2 { // Compute the angle against the updated anchor position - let d_bob = bob - anchor; - self.vars.θ = f32::atan2(-d_bob.x, d_bob.y); + let d_bob = self.bob - anchor; + self.state.vars.θ = f32::atan2(-d_bob.x, d_bob.y); // Run the pendulum simulation in terms of angle - runge_kutta::tick(&eval, self, (puppet_physics, props), anchor, dt); + self.state.tick(&eval, (props.0, &props.1.props), &anchor, t, dt); // Update the bob position at the new angle - let angle = self.vars.θ; + let angle = self.state.vars.θ; let d_bob = vec2(-angle.sin(), angle.cos()); - anchor + d_bob * props.length + anchor + d_bob * props.1.props.length } } +/// Acceleration of bob caused by gravity. fn eval( - state: &mut PhysicsState, - &(puppet_physics, props): &(PuppetPhysics, &PhysicsProps), - _anchor: Vec2, + state: &mut PhysicsState<2, RigidPendulum>, + (puppet_physics, props): &(&PuppetPhysics, &PhysicsProps), + _anchor: &Vec2, _t: f32, ) { // https://www.myphysicslab.com/pendulum/pendulum-en.html diff --git a/inox2d/src/physics/pendulum/spring.rs b/inox2d/src/physics/pendulum/spring.rs index 6538acc..c442cec 100644 --- a/inox2d/src/physics/pendulum/spring.rs +++ b/inox2d/src/physics/pendulum/spring.rs @@ -1,32 +1,18 @@ -use crate::node::data::PhysicsProps; -use crate::physics::runge_kutta::{self, IsPhysicsVars, PhysicsState}; -use crate::puppet::PuppetPhysics; -use glam::{vec2, Vec2}; use std::f32::consts::PI; -#[repr(C)] -#[derive(Debug, Clone, Copy, Default)] -pub struct SpringPendulum { - pub bob_pos: Vec2, - pub bob_vel: Vec2, -} - -impl PhysicsState { - pub(crate) fn tick( - &mut self, - puppet_physics: PuppetPhysics, - props: &PhysicsProps, - bob: Vec2, - anchor: Vec2, - dt: f32, - ) -> Vec2 { - self.vars.bob_pos = bob; +use glam::{vec2, Vec2}; - // Run the spring pendulum simulation - runge_kutta::tick(&eval, self, (puppet_physics, props), anchor, dt); +use crate::node::components::simple_physics::{PhysicsProps, SpringPendulumCtx}; +use crate::physics::{ + pendulum::Pendulum, + runge_kutta::{IsPhysicsVars, PhysicsState}, + PuppetPhysics, SimplePhysicsProps, +}; - self.vars.bob_pos - } +/// Variables for Runge-Kutta method. +pub(crate) struct SpringPendulum { + pub bob_pos: Vec2, + pub bob_vel: Vec2, } impl IsPhysicsVars<4> for SpringPendulum { @@ -39,10 +25,30 @@ impl IsPhysicsVars<4> for SpringPendulum { } } +impl Pendulum for SpringPendulumCtx { + fn get_bob(&self) -> Vec2 { + self.state.vars.bob_pos + } + + fn set_bob(&mut self, bob: Vec2) { + self.state.vars.bob_pos = bob; + } + + fn tick(&mut self, props: &SimplePhysicsProps, anchor: Vec2, t: f32, dt: f32) -> Vec2 { + // Run the spring pendulum simulation + self.state.tick(&eval, (props.0, &props.1.props), &anchor, t, dt); + + self.state.vars.bob_pos + } +} + +/// Acceleration of bob caused by both +/// - gravity. +/// - damped oscillation of the spring-bob system in the radial direction. fn eval( - state: &mut PhysicsState, - &(puppet_physics, props): &(PuppetPhysics, &PhysicsProps), - anchor: Vec2, + state: &mut PhysicsState<4, SpringPendulum>, + &(puppet_physics, props): &(&PuppetPhysics, &PhysicsProps), + anchor: &Vec2, _t: f32, ) { state.derivatives.bob_pos = state.vars.bob_vel; @@ -54,7 +60,7 @@ fn eval( let g = props.gravity * puppet_physics.pixels_per_meter * puppet_physics.gravity; let rest_length = props.length - g / spring_k; - let off_pos = state.vars.bob_pos - anchor; + let off_pos = state.vars.bob_pos - *anchor; let off_pos_norm = off_pos.normalize(); let length_ratio = g / props.length; diff --git a/inox2d/src/physics/runge_kutta.rs b/inox2d/src/physics/runge_kutta.rs index 7e982bc..6aa6905 100644 --- a/inox2d/src/physics/runge_kutta.rs +++ b/inox2d/src/physics/runge_kutta.rs @@ -1,66 +1,61 @@ -use glam::Vec2; - -pub trait IsPhysicsVars { +pub(crate) trait IsPhysicsVars { fn get_f32s(&self) -> [f32; N]; fn set_f32s(&mut self, f32s: [f32; N]); } -#[derive(Debug, Clone, Copy, Default)] -pub struct PhysicsState { +pub(crate) struct PhysicsState> { pub vars: T, pub derivatives: T, - pub t: f32, } -pub fn tick, P>( - eval: &impl Fn(&mut PhysicsState, &P, Vec2, f32), - phys: &mut PhysicsState, - props: P, - anchor: Vec2, - h: f32, -) { - let curs = phys.vars.get_f32s(); - phys.derivatives.set_f32s([0.; N]); - - let t = phys.t; - - (eval)(phys, &props, anchor, t); - let k1s = phys.derivatives.get_f32s(); - - let mut vars = [0.; N]; - for i in 0..N { - vars[i] = curs[i] + h * k1s[i] / 2.; - } - phys.vars.set_f32s(vars); - (eval)(phys, &props, anchor, t + h / 2.); - let k2s = phys.derivatives.get_f32s(); - - let mut vars = [0.; N]; - for i in 0..N { - vars[i] = curs[i] + h * k2s[i] / 2.; - } - phys.vars.set_f32s(vars); - (eval)(phys, &props, anchor, t + h / 2.); - let k3s = phys.derivatives.get_f32s(); +impl> PhysicsState { + pub fn tick( + &mut self, + eval: &impl Fn(&mut PhysicsState, &P, &A, f32), + props: P, + anchor: &A, + t: f32, + h: f32, + ) { + let curs = self.vars.get_f32s(); + self.derivatives.set_f32s([0.; N]); + + (eval)(self, &props, anchor, t); + let k1s = self.derivatives.get_f32s(); + + let mut vars = [0.; N]; + for i in 0..N { + vars[i] = curs[i] + h * k1s[i] / 2.; + } + self.vars.set_f32s(vars); + (eval)(self, &props, anchor, t + h / 2.); + let k2s = self.derivatives.get_f32s(); - let mut vars = [0.; N]; - for i in 0..N { - vars[i] = curs[i] + h * k3s[i]; - } - phys.vars.set_f32s(vars); - (eval)(phys, &props, anchor, t + h); - let k4s = phys.derivatives.get_f32s(); + let mut vars = [0.; N]; + for i in 0..N { + vars[i] = curs[i] + h * k2s[i] / 2.; + } + self.vars.set_f32s(vars); + (eval)(self, &props, anchor, t + h / 2.); + let k3s = self.derivatives.get_f32s(); - let mut vars = [0.; N]; - for i in 0..N { - vars[i] = curs[i] + h * (k1s[i] + 2. * k2s[i] + 2. * k3s[i] + k4s[i]) / 6.; - if !vars[i].is_finite() { - // Simulation failed, revert - vars = curs; - break; + let mut vars = [0.; N]; + for i in 0..N { + vars[i] = curs[i] + h * k3s[i]; + } + self.vars.set_f32s(vars); + (eval)(self, &props, anchor, t + h); + let k4s = self.derivatives.get_f32s(); + + let mut vars = [0.; N]; + for i in 0..N { + vars[i] = curs[i] + h * (k1s[i] + 2. * k2s[i] + 2. * k3s[i] + k4s[i]) / 6.; + if !vars[i].is_finite() { + // Simulation failed, revert + vars = curs; + break; + } } + self.vars.set_f32s(vars); } - phys.vars.set_f32s(vars); - - phys.t += h; } diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index f3ee93a..1d819cf 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -7,6 +7,7 @@ use std::collections::HashMap; use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{Param, ParamCtx}; +use crate::physics::PuppetPhysics; use crate::render::RenderCtx; use meta::PuppetMeta; @@ -121,9 +122,3 @@ impl Puppet { } } } - -/// Global physics parameters for the puppet. -pub struct PuppetPhysics { - pub pixels_per_meter: f32, - pub gravity: f32, -} From d3c3d02d7fd301973385a37d7864544c9acebd83 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Wed, 31 Jul 2024 20:46:02 +0800 Subject: [PATCH 23/34] Physics back online. The implementation is inefficient (check changes in `puppet.rs`), yes, but clear, and the bottleneck is not here anyways. Cloning of `SimplePhysics` is required for working around ownership problems, until dark magic implemented on `World` enabling multiple mutable handles to different components. --- examples/render-opengl/src/main.rs | 3 + inox2d/src/math/transform.rs | 2 +- inox2d/src/node/components/simple_physics.rs | 6 ++ inox2d/src/node/components/transform_store.rs | 2 +- inox2d/src/physics.rs | 88 ++++++++++++++++++- inox2d/src/physics/pendulum/rigid.rs | 1 + inox2d/src/physics/pendulum/spring.rs | 1 + inox2d/src/physics/runge_kutta.rs | 5 +- inox2d/src/puppet.rs | 54 +++++++++++- inox2d/src/puppet/transforms.rs | 2 +- 10 files changed, 154 insertions(+), 10 deletions(-) diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index 6e4de02..436c506 100644 --- a/examples/render-opengl/src/main.rs +++ b/examples/render-opengl/src/main.rs @@ -51,6 +51,7 @@ fn main() -> Result<(), Box> { model.puppet.init_transforms(); model.puppet.init_rendering(); model.puppet.init_params(); + model.puppet.init_physics(); tracing::info!("Setting up windowing and OpenGL"); let app_frame = AppFrame::init( @@ -130,6 +131,8 @@ impl App for Inox2dOpenglExampleApp { .as_mut() .unwrap() .set("Head:: Yaw-Pitch", Vec2::new(t.cos(), t.sin())); + // Actually, not providing 0 for the first frame will not create too big a problem. + // Just that physics simulation will run for the provided time, which may be big and causes a startup delay. puppet.end_frame(scene_ctrl.dt()); inox2d::render::draw(renderer, puppet); diff --git a/inox2d/src/math/transform.rs b/inox2d/src/math/transform.rs index 51e60cb..a46ad17 100644 --- a/inox2d/src/math/transform.rs +++ b/inox2d/src/math/transform.rs @@ -1,7 +1,7 @@ use glam::{EulerRot, Mat4, Quat, Vec2, Vec3}; /// relative transform -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone)] pub struct TransformOffset { /// X Y Z pub translation: Vec3, diff --git a/inox2d/src/node/components/simple_physics.rs b/inox2d/src/node/components/simple_physics.rs index 4f8faac..5bc9544 100644 --- a/inox2d/src/node/components/simple_physics.rs +++ b/inox2d/src/node/components/simple_physics.rs @@ -7,6 +7,7 @@ use crate::physics::{ }; /// If has this as a component, the node is capable of doing Inochi2D SimplePhysics simulations +#[derive(Clone)] pub struct SimplePhysics { pub param: ParamUuid, pub model_type: PhysicsModel, @@ -16,16 +17,19 @@ pub struct SimplePhysics { pub local_only: bool, } +#[derive(Clone)] pub enum PhysicsModel { RigidPendulum, SpringPendulum, } +#[derive(Clone)] pub enum ParamMapMode { AngleLength, XY, } +#[derive(Clone)] pub struct PhysicsProps { /// Gravity scale (1.0 = puppet gravity) pub gravity: f32, @@ -54,12 +58,14 @@ impl Default for PhysicsProps { } /// Physical states for simulating a rigid pendulum. +#[derive(Default)] pub(crate) struct RigidPendulumCtx { pub bob: Vec2, pub state: PhysicsState<2, RigidPendulum>, } /// Physical states for simulating a spring pendulum. +#[derive(Default)] pub(crate) struct SpringPendulumCtx { pub state: PhysicsState<4, SpringPendulum>, } diff --git a/inox2d/src/node/components/transform_store.rs b/inox2d/src/node/components/transform_store.rs index 3579eb4..0078065 100644 --- a/inox2d/src/node/components/transform_store.rs +++ b/inox2d/src/node/components/transform_store.rs @@ -3,7 +3,7 @@ use glam::Mat4; use crate::math::transform::TransformOffset; /// Component holding transform values that may be modified across frames. -#[derive(Default)] +#[derive(Default, Clone)] pub struct TransformStore { pub absolute: Mat4, pub relative: TransformOffset, diff --git a/inox2d/src/physics.rs b/inox2d/src/physics.rs index ae0e617..5466d43 100644 --- a/inox2d/src/physics.rs +++ b/inox2d/src/physics.rs @@ -1,9 +1,16 @@ pub mod pendulum; pub(crate) mod runge_kutta; +use std::collections::HashMap; + use glam::Vec2; -use crate::node::components::{SimplePhysics, TransformStore}; +use crate::node::components::{ + simple_physics::{PhysicsModel, RigidPendulumCtx, SpringPendulumCtx}, + SimplePhysics, TransformStore, +}; +use crate::params::ParamUuid; +use crate::puppet::{InoxNodeTree, Puppet, World}; /// Global physics parameters for the puppet. pub struct PuppetPhysics { @@ -53,3 +60,82 @@ impl SimplePhysicsCtxCommon for T { self.calc_output(props, transform, anchor) } } + +/// Additional struct attached to a puppet for executing all physics nodes. +pub(crate) struct PhysicsCtx { + /// Time since first simulation step. + t: f32, + param_uuid_to_name: HashMap, +} + +impl PhysicsCtx { + /// MODIFIES puppet. In addition to initializing self, installs physics contexts in the World of components + pub fn new(puppet: &mut Puppet) -> Self { + for node in puppet.nodes.iter() { + if let Some(simple_physics) = puppet.node_comps.get::(node.uuid) { + match simple_physics.model_type { + PhysicsModel::RigidPendulum => puppet.node_comps.add(node.uuid, RigidPendulumCtx::default()), + PhysicsModel::SpringPendulum => puppet.node_comps.add(node.uuid, SpringPendulumCtx::default()), + } + } + } + + Self { + t: 0., + param_uuid_to_name: puppet.params.iter().map(|p| (p.1.uuid, p.0.to_owned())).collect(), + } + } + + pub fn step( + &mut self, + puppet_physics: &PuppetPhysics, + nodes: &InoxNodeTree, + comps: &mut World, + dt: f32, + ) -> HashMap { + let mut values_to_apply = HashMap::new(); + + if dt == 0. { + return values_to_apply; + } else if dt < 0. { + panic!("Time travel has happened."); + } + + for node in nodes.iter() { + if let Some(simple_physics) = comps.get::(node.uuid) { + // before we use some Rust dark magic so that two components can be mutably borrowed at the same time, + // need to clone to workaround comps ownership problem + let simple_physics = simple_physics.clone(); + let props = &(puppet_physics, &simple_physics); + let transform = &comps + .get::(node.uuid) + .expect("All nodes with SimplePhysics must have associated TransformStore.") + .clone(); + + let param_value = if let Some(rigid_pendulum_ctx) = comps.get_mut::(node.uuid) { + Some(rigid_pendulum_ctx.update(props, transform, self.t, dt)) + } else if let Some(spring_pendulum_ctx) = comps.get_mut::(node.uuid) { + Some(spring_pendulum_ctx.update(props, transform, self.t, dt)) + } else { + None + }; + + if let Some(param_value) = param_value { + values_to_apply + .entry( + self.param_uuid_to_name + .get(&simple_physics.param) + .expect("A SimplePhysics node must reference a valid param.") + .to_owned(), + ) + .and_modify(|_| panic!("Two SimplePhysics nodes reference a same param.")) + .or_insert(param_value); + } + } + } + + self.t += dt; + + values_to_apply + } +} diff --git a/inox2d/src/physics/pendulum/rigid.rs b/inox2d/src/physics/pendulum/rigid.rs index ae82f4d..96d2e96 100644 --- a/inox2d/src/physics/pendulum/rigid.rs +++ b/inox2d/src/physics/pendulum/rigid.rs @@ -8,6 +8,7 @@ use crate::physics::{ }; /// Variables for Runge-Kutta method. +#[derive(Default)] pub(crate) struct RigidPendulum { pub θ: f32, pub ω: f32, diff --git a/inox2d/src/physics/pendulum/spring.rs b/inox2d/src/physics/pendulum/spring.rs index c442cec..62b8c2b 100644 --- a/inox2d/src/physics/pendulum/spring.rs +++ b/inox2d/src/physics/pendulum/spring.rs @@ -10,6 +10,7 @@ use crate::physics::{ }; /// Variables for Runge-Kutta method. +#[derive(Default)] pub(crate) struct SpringPendulum { pub bob_pos: Vec2, pub bob_vel: Vec2, diff --git a/inox2d/src/physics/runge_kutta.rs b/inox2d/src/physics/runge_kutta.rs index 6aa6905..a99ec74 100644 --- a/inox2d/src/physics/runge_kutta.rs +++ b/inox2d/src/physics/runge_kutta.rs @@ -3,12 +3,13 @@ pub(crate) trait IsPhysicsVars { fn set_f32s(&mut self, f32s: [f32; N]); } -pub(crate) struct PhysicsState> { +#[derive(Default)] +pub(crate) struct PhysicsState> { pub vars: T, pub derivatives: T, } -impl> PhysicsState { +impl> PhysicsState { pub fn tick( &mut self, eval: &impl Fn(&mut PhysicsState, &P, &A, f32), diff --git a/inox2d/src/puppet.rs b/inox2d/src/puppet.rs index 1d819cf..ce67ff8 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{Param, ParamCtx}; -use crate::physics::PuppetPhysics; +use crate::physics::{PhysicsCtx, PuppetPhysics}; use crate::render::RenderCtx; use meta::PuppetMeta; @@ -19,7 +19,7 @@ pub use world::World; pub struct Puppet { pub meta: PuppetMeta, physics: PuppetPhysics, - // TODO: define the actual ctx for physics + physics_ctx: Option, pub(crate) nodes: InoxNodeTree, pub(crate) node_comps: World, /// Currently only a marker for if transform/zsort components are initialized. @@ -41,6 +41,7 @@ impl Puppet { Self { meta, physics, + physics_ctx: None, nodes: InoxNodeTree::new_with_root(root), node_comps: World::new(), transform_ctx: None, @@ -65,7 +66,7 @@ impl Puppet { /// - on second call. pub fn init_rendering(&mut self) { if self.transform_ctx.is_none() { - panic!("Puppet rendering depends on initialized puppet transforms.") + panic!("Puppet rendering depends on initialized puppet transforms."); } if self.render_ctx.is_some() { panic!("Puppet already initialized for rendering."); @@ -90,6 +91,21 @@ impl Puppet { self.param_ctx = Some(param_ctx); } + /// Call this on a puppet if physics are going to be simulated. Panicks: + /// - if params is not initialized. + /// - on second call. + pub fn init_physics(&mut self) { + if self.param_ctx.is_none() { + panic!("Puppet physics depends on initialized puppet params."); + } + if self.physics_ctx.is_some() { + panic!("Puppet already initialized for physics."); + } + + let physics_ctx = PhysicsCtx::new(self); + self.physics_ctx = Some(physics_ctx); + } + /// Prepare the puppet for a new frame. User may set params afterwards. pub fn begin_frame(&mut self) { if let Some(render_ctx) = self.render_ctx.as_mut() { @@ -108,7 +124,7 @@ impl Puppet { /// Freeze puppet for one frame. Rendering, if initialized, may follow. /// /// Provide elapsed time for physics, if initialized, to run. Provide `0` for the first call. - pub fn end_frame(&mut self, _dt: f32) { + pub fn end_frame(&mut self, dt: f32) { if let Some(param_ctx) = self.param_ctx.as_mut() { param_ctx.apply(&self.params, &mut self.node_comps); } @@ -117,6 +133,36 @@ impl Puppet { transform_ctx.update(&self.nodes, &mut self.node_comps); } + if let Some(physics_ctx) = self.physics_ctx.as_mut() { + let values_to_apply = physics_ctx.step(&self.physics, &self.nodes, &mut self.node_comps, dt); + + // TODO: Think about separating DeformStack reset and RenderCtx reset? + self.render_ctx + .as_mut() + .expect("If physics is initialized, so does params, so does rendering.") + .reset(&self.nodes, &mut self.node_comps); + + // TODO: Fewer repeated calculations of a same transform? + let transform_ctx = self + .transform_ctx + .as_mut() + .expect("If physics is initialized, so does transforms."); + transform_ctx.reset(&self.nodes, &mut self.node_comps); + + let param_ctx = self + .param_ctx + .as_mut() + .expect("If physics is initialized, so does params."); + for (param_name, value) in &values_to_apply { + param_ctx + .set(param_name, *value) + .expect("Param name returned by .step() must exist."); + } + param_ctx.apply(&self.params, &mut self.node_comps); + + transform_ctx.update(&self.nodes, &mut self.node_comps); + } + if let Some(render_ctx) = self.render_ctx.as_mut() { render_ctx.update(&self.nodes, &mut self.node_comps); } diff --git a/inox2d/src/puppet/transforms.rs b/inox2d/src/puppet/transforms.rs index 0c38fb6..ac11a15 100644 --- a/inox2d/src/puppet/transforms.rs +++ b/inox2d/src/puppet/transforms.rs @@ -17,7 +17,7 @@ impl TransformCtx { /// Reset all transform/zsort values to default. pub fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { for node in nodes.iter() { - comps.get_mut::(node.uuid).unwrap().relative = node.trans_offset; + comps.get_mut::(node.uuid).unwrap().relative = node.trans_offset.clone(); comps.get_mut::(node.uuid).unwrap().0 = node.zsort; } } From 51a2c4a9f052fbb351a176b6f094f89132ab0c14 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sat, 3 Aug 2024 22:36:28 +0800 Subject: [PATCH 24/34] `DeformSrc` -> `DeformSource` And suppress variant unused warning... until Meshgroups. --- inox2d/src/node/components/deform_stack.rs | 7 ++++--- inox2d/src/params.rs | 4 ++-- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/inox2d/src/node/components/deform_stack.rs b/inox2d/src/node/components/deform_stack.rs index ac011b5..ed8e466 100644 --- a/inox2d/src/node/components/deform_stack.rs +++ b/inox2d/src/node/components/deform_stack.rs @@ -10,7 +10,8 @@ use crate::puppet::{InoxNodeTree, World}; /// Source of a deform. #[derive(Hash, PartialEq, Eq, Copy, Clone)] -pub(crate) enum DeformSrc { +#[allow(unused)] +pub(crate) enum DeformSource { Param(ParamUuid), Node(InoxNodeUuid), } @@ -24,7 +25,7 @@ pub(crate) struct DeformStack { deform_len: usize, /// map of (src, (enabled, Deform)). /// On reset, only set enabled to false instead of clearing the map, as deforms from same sources tend to come in every frame. - stack: HashMap, + stack: HashMap, } impl DeformStack { @@ -60,7 +61,7 @@ impl DeformStack { } /// Submit a deform from a source for a node. - pub(crate) fn push(&mut self, src: DeformSrc, mut deform: Deform) { + pub(crate) fn push(&mut self, src: DeformSource, mut deform: Deform) { let Deform::Direct(ref direct_deform) = deform; if direct_deform.len() != self.deform_len { panic!("A direct deform with non-matching dimensions is submitted to a node."); diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index f831e8f..50db2b3 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -8,7 +8,7 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{deform_stack::DeformSrc, DeformStack, TexturedMesh, TransformStore, ZSort}, + components::{deform_stack::DeformSource, DeformStack, TexturedMesh, TransformStore, ZSort}, InoxNodeUuid, }; use crate::puppet::{Puppet, World}; @@ -210,7 +210,7 @@ impl Param { comps .get_mut::(binding.node) .expect("Nodes being deformed must have a DeformStack component.") - .push(DeformSrc::Param(self.uuid), Deform::Direct(direct_deform)); + .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } } } From fa1ab365c353610625440f7cfb0e9af253bd5bc2 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sat, 3 Aug 2024 23:34:08 +0800 Subject: [PATCH 25/34] Put all components into `compoents.rs`. Responding to https://github.com/Inochi2D/inox2d/pull/86#discussion_r1698615893 Rename `ParamMapMode` to `PhysicsParamMapMode` for this change for clarity. --- inox2d-opengl/src/lib.rs | 5 +- inox2d/src/formats/payload.rs | 6 +- inox2d/src/node/components.rs | 248 ++++++++++++++++-- inox2d/src/node/components/composite.rs | 4 - inox2d/src/node/components/drawable.rs | 83 ------ inox2d/src/node/components/simple_physics.rs | 71 ----- inox2d/src/node/components/textured_mesh.rs | 22 -- inox2d/src/node/components/transform_store.rs | 10 - inox2d/src/node/components/zsort.rs | 15 -- inox2d/src/params.rs | 2 +- inox2d/src/physics.rs | 5 +- inox2d/src/physics/pendulum.rs | 6 +- inox2d/src/physics/pendulum/rigid.rs | 2 +- inox2d/src/physics/pendulum/spring.rs | 2 +- inox2d/src/render.rs | 6 +- .../components => render}/deform_stack.rs | 23 +- inox2d/src/render/vertex_buffers.rs | 2 +- 17 files changed, 246 insertions(+), 266 deletions(-) delete mode 100644 inox2d/src/node/components/composite.rs delete mode 100644 inox2d/src/node/components/drawable.rs delete mode 100644 inox2d/src/node/components/simple_physics.rs delete mode 100644 inox2d/src/node/components/textured_mesh.rs delete mode 100644 inox2d/src/node/components/transform_store.rs delete mode 100644 inox2d/src/node/components/zsort.rs rename inox2d/src/{node/components => render}/deform_stack.rs (65%) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 398aafe..27cd07d 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -13,10 +13,7 @@ use glow::HasContext; use inox2d::math::camera::Camera; use inox2d::model::Model; use inox2d::node::{ - components::{ - drawable::{BlendMode, Mask, MaskMode, Masks}, - TexturedMesh, - }, + components::{BlendMode, Mask, MaskMode, Masks, TexturedMesh}, drawables::{CompositeComponents, TexturedMeshComponents}, InoxNodeUuid, }; diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 7568f22..7bf48b8 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -6,7 +6,7 @@ use json::JsonValue; use crate::math::interp::InterpolateMode; use crate::math::matrix::{Matrix2d, Matrix2dFromSliceVecsError}; use crate::math::transform::TransformOffset; -use crate::node::components::{drawable::*, simple_physics::*, textured_mesh::*, *}; +use crate::node::components::*; use crate::node::{InoxNode, InoxNodeUuid}; use crate::params::{AxisPoints, Binding, BindingValues, Param, ParamUuid}; use crate::physics::PuppetPhysics; @@ -156,8 +156,8 @@ fn deserialize_simple_physics(obj: JsonObject) -> InoxParseResult a => todo!("{}", a), }, map_mode: match obj.get_str("map_mode")? { - "AngleLength" => ParamMapMode::AngleLength, - "XY" => ParamMapMode::XY, + "AngleLength" => PhysicsParamMapMode::AngleLength, + "XY" => PhysicsParamMapMode::XY, unknown => return Err(InoxParseError::UnknownParamMapMode(unknown.to_owned())), }, diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index e344334..8110780 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -7,25 +7,239 @@ Inochi2D node types to Inox2D components: - Custom nodes by inheritance -> Custom nodes by composition */ -pub mod composite; -pub mod drawable; -pub mod simple_physics; -pub mod textured_mesh; +use glam::{Mat4, Vec2, Vec3}; + +use crate::math::deform::Deform; +use crate::node::{InoxNodeUuid, TransformOffset}; +use crate::params::ParamUuid; +use crate::physics::{ + pendulum::{rigid::RigidPendulum, spring::SpringPendulum}, + runge_kutta::PhysicsState, +}; +use crate::texture::TextureId; + +/* --- COMPOSITE --- */ + +/// If has this as a component, the node should composite all children +/// +/// Empty as only a marker, zsorted children list constructed later on demand +pub struct Composite {} + +/* --- DRAWABLE --- */ + +/// If has this as a component, the node should render something +pub struct Drawable { + pub blending: Blending, + /// If Some, the node should consider masking when rendering + pub masks: Option, +} + +pub struct Blending { + pub mode: BlendMode, + pub tint: Vec3, + pub screen_tint: Vec3, + pub opacity: f32, +} + +#[derive(Default, PartialEq, Clone, Copy)] +pub enum BlendMode { + /// Normal blending mode. + #[default] + Normal, + /// Multiply blending mode. + Multiply, + /// Color Dodge. + ColorDodge, + /// Linear Dodge. + LinearDodge, + /// Screen. + Screen, + /// Clip to Lower. + /// Special blending mode that clips the drawable + /// to a lower rendered area. + ClipToLower, + /// Slice from Lower. + /// Special blending mode that slices the drawable + /// via a lower rendered area. + /// (Basically inverse ClipToLower.) + SliceFromLower, +} + +impl BlendMode { + pub const VALUES: [BlendMode; 7] = [ + BlendMode::Normal, + BlendMode::Multiply, + BlendMode::ColorDodge, + BlendMode::LinearDodge, + BlendMode::Screen, + BlendMode::ClipToLower, + BlendMode::SliceFromLower, + ]; +} + +pub struct Masks { + pub threshold: f32, + pub masks: Vec, +} + +impl Masks { + /// Checks whether has masks of mode `MaskMode::Mask`. + pub fn has_masks(&self) -> bool { + self.masks.iter().any(|mask| mask.mode == MaskMode::Mask) + } + + /// Checks whether has masks of mode `MaskMode::Dodge`. + pub fn has_dodge_masks(&self) -> bool { + self.masks.iter().any(|mask| mask.mode == MaskMode::Dodge) + } +} + +pub struct Mask { + pub source: InoxNodeUuid, + pub mode: MaskMode, +} + +#[derive(PartialEq)] +pub enum MaskMode { + /// The part should be masked by the drawables specified. + Mask, + /// The path should be dodge-masked by the drawables specified. + Dodge, +} + +/* --- SIMPLE PHYSICS --- */ + +/// If has this as a component, the node is capable of doing Inochi2D SimplePhysics simulations +#[derive(Clone)] +pub struct SimplePhysics { + pub param: ParamUuid, + pub model_type: PhysicsModel, + pub map_mode: PhysicsParamMapMode, + pub props: PhysicsProps, + /// Whether physics system listens to local transform only. + pub local_only: bool, +} + +#[derive(Clone)] +pub enum PhysicsModel { + RigidPendulum, + SpringPendulum, +} + +#[derive(Clone)] +pub enum PhysicsParamMapMode { + AngleLength, + XY, +} + +#[derive(Clone)] +pub struct PhysicsProps { + /// Gravity scale (1.0 = puppet gravity) + pub gravity: f32, + /// Pendulum/spring rest length (pixels) + pub length: f32, + /// Resonant frequency (Hz) + pub frequency: f32, + /// Angular damping ratio + pub angle_damping: f32, + /// Length damping ratio + pub length_damping: f32, + pub output_scale: Vec2, +} + +impl Default for PhysicsProps { + fn default() -> Self { + Self { + gravity: 1., + length: 1., + frequency: 1., + angle_damping: 0.5, + length_damping: 0.5, + output_scale: Vec2::ONE, + } + } +} + +/// Physical states for simulating a rigid pendulum. +#[derive(Default)] +pub(crate) struct RigidPendulumCtx { + pub bob: Vec2, + pub state: PhysicsState<2, RigidPendulum>, +} + +/// Physical states for simulating a spring pendulum. +#[derive(Default)] +pub(crate) struct SpringPendulumCtx { + pub state: PhysicsState<4, SpringPendulum>, +} + +/* --- TEXTURED MESH --- */ + +/// If has this as a component, the node should render a deformed texture +pub struct TexturedMesh { + pub mesh: Mesh, + pub tex_albedo: TextureId, + pub tex_emissive: TextureId, + pub tex_bumpmap: TextureId, +} + +pub struct Mesh { + /// Vertices in the mesh. + pub vertices: Vec, + /// Base UVs. + pub uvs: Vec, + /// Indices in the mesh. + pub indices: Vec, + /// Origin of the mesh. + pub origin: Vec2, +} + +/* --- DEFORM STACK --- */ + +/// Source of a deform. +#[derive(Hash, PartialEq, Eq, Copy, Clone)] +#[allow(unused)] +pub(crate) enum DeformSource { + Param(ParamUuid), + Node(InoxNodeUuid), +} /// Internal component solving for deforms of a node. -pub(crate) mod deform_stack; +/// Storing deforms specified by multiple sources to apply on one node for one frame. +/// +/// Despite the name (this is respecting the ref impl), this is not in any way a stack. +/// The order of deforms being applied, or more generally speaking, the way multiple deforms adds up to be a single one, needs to be defined according to the spec. +pub(crate) struct DeformStack { + /// this is a component so cannot use generics for the length. + pub(crate) deform_len: usize, + /// map of (src, (enabled, Deform)). + /// On reset, only set enabled to false instead of clearing the map, as deforms from same sources tend to come in every frame. + pub(crate) stack: std::collections::HashMap, +} + +/* --- TRANSFORM STORE --- */ + /// Internal component storing: /// - Relative transform being determined in between frames. /// - Absolute transform prepared from all relative transforms just before rendering. -pub(crate) mod transform_store; -/// Internal component storing zsort being determined in between frames. -pub(crate) mod zsort; - -pub use composite::Composite; -pub use drawable::Drawable; -pub use simple_physics::SimplePhysics; -pub use textured_mesh::TexturedMesh; - -pub(crate) use deform_stack::DeformStack; -pub(crate) use transform_store::TransformStore; -pub(crate) use zsort::ZSort; +#[derive(Default, Clone)] +pub struct TransformStore { + pub absolute: Mat4, + pub relative: TransformOffset, +} + +/* --- ZSORT --- */ + +/// Component holding zsort values that may be modified across frames. +// only one value instead of absolute + relative as in TransformStore, cause inheritance of zsort (+) is commutative +#[derive(Default)] +pub(crate) struct ZSort(pub f32); + +// so ZSort automatically gets the `.total_cmp()` of `f32` +impl std::ops::Deref for ZSort { + type Target = f32; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} diff --git a/inox2d/src/node/components/composite.rs b/inox2d/src/node/components/composite.rs deleted file mode 100644 index 8eede65..0000000 --- a/inox2d/src/node/components/composite.rs +++ /dev/null @@ -1,4 +0,0 @@ -/// If has this as a component, the node should composite all children -/// -/// Empty as only a marker, zsorted children list constructed later on demand -pub struct Composite {} diff --git a/inox2d/src/node/components/drawable.rs b/inox2d/src/node/components/drawable.rs deleted file mode 100644 index 8033c57..0000000 --- a/inox2d/src/node/components/drawable.rs +++ /dev/null @@ -1,83 +0,0 @@ -use glam::Vec3; - -use super::super::InoxNodeUuid; - -/// If has this as a component, the node should render something -pub struct Drawable { - pub blending: Blending, - /// If Some, the node should consider masking when rendering - pub masks: Option, -} - -pub struct Blending { - pub mode: BlendMode, - pub tint: Vec3, - pub screen_tint: Vec3, - pub opacity: f32, -} - -#[derive(Default, PartialEq, Clone, Copy)] -pub enum BlendMode { - /// Normal blending mode. - #[default] - Normal, - /// Multiply blending mode. - Multiply, - /// Color Dodge. - ColorDodge, - /// Linear Dodge. - LinearDodge, - /// Screen. - Screen, - /// Clip to Lower. - /// Special blending mode that clips the drawable - /// to a lower rendered area. - ClipToLower, - /// Slice from Lower. - /// Special blending mode that slices the drawable - /// via a lower rendered area. - /// (Basically inverse ClipToLower.) - SliceFromLower, -} - -impl BlendMode { - pub const VALUES: [BlendMode; 7] = [ - BlendMode::Normal, - BlendMode::Multiply, - BlendMode::ColorDodge, - BlendMode::LinearDodge, - BlendMode::Screen, - BlendMode::ClipToLower, - BlendMode::SliceFromLower, - ]; -} - -pub struct Masks { - pub threshold: f32, - pub masks: Vec, -} - -impl Masks { - /// Checks whether has masks of mode `MaskMode::Mask`. - pub fn has_masks(&self) -> bool { - self.masks.iter().any(|mask| mask.mode == MaskMode::Mask) - } - - /// Checks whether has masks of mode `MaskMode::Dodge`. - pub fn has_dodge_masks(&self) -> bool { - self.masks.iter().any(|mask| mask.mode == MaskMode::Dodge) - } -} - -pub struct Mask { - pub source: InoxNodeUuid, - pub mode: MaskMode, -} - -#[derive(PartialEq)] -pub enum MaskMode { - /// The part should be masked by the drawables specified. - Mask, - /// The path should be dodge-masked by the drawables specified. - Dodge, -} diff --git a/inox2d/src/node/components/simple_physics.rs b/inox2d/src/node/components/simple_physics.rs deleted file mode 100644 index 5bc9544..0000000 --- a/inox2d/src/node/components/simple_physics.rs +++ /dev/null @@ -1,71 +0,0 @@ -use glam::Vec2; - -use crate::params::ParamUuid; -use crate::physics::{ - pendulum::{rigid::RigidPendulum, spring::SpringPendulum}, - runge_kutta::PhysicsState, -}; - -/// If has this as a component, the node is capable of doing Inochi2D SimplePhysics simulations -#[derive(Clone)] -pub struct SimplePhysics { - pub param: ParamUuid, - pub model_type: PhysicsModel, - pub map_mode: ParamMapMode, - pub props: PhysicsProps, - /// Whether physics system listens to local transform only. - pub local_only: bool, -} - -#[derive(Clone)] -pub enum PhysicsModel { - RigidPendulum, - SpringPendulum, -} - -#[derive(Clone)] -pub enum ParamMapMode { - AngleLength, - XY, -} - -#[derive(Clone)] -pub struct PhysicsProps { - /// Gravity scale (1.0 = puppet gravity) - pub gravity: f32, - /// Pendulum/spring rest length (pixels) - pub length: f32, - /// Resonant frequency (Hz) - pub frequency: f32, - /// Angular damping ratio - pub angle_damping: f32, - /// Length damping ratio - pub length_damping: f32, - pub output_scale: Vec2, -} - -impl Default for PhysicsProps { - fn default() -> Self { - Self { - gravity: 1., - length: 1., - frequency: 1., - angle_damping: 0.5, - length_damping: 0.5, - output_scale: Vec2::ONE, - } - } -} - -/// Physical states for simulating a rigid pendulum. -#[derive(Default)] -pub(crate) struct RigidPendulumCtx { - pub bob: Vec2, - pub state: PhysicsState<2, RigidPendulum>, -} - -/// Physical states for simulating a spring pendulum. -#[derive(Default)] -pub(crate) struct SpringPendulumCtx { - pub state: PhysicsState<4, SpringPendulum>, -} diff --git a/inox2d/src/node/components/textured_mesh.rs b/inox2d/src/node/components/textured_mesh.rs deleted file mode 100644 index fcadafb..0000000 --- a/inox2d/src/node/components/textured_mesh.rs +++ /dev/null @@ -1,22 +0,0 @@ -use glam::Vec2; - -use crate::texture::TextureId; - -/// If has this as a component, the node should render a deformed texture -pub struct TexturedMesh { - pub mesh: Mesh, - pub tex_albedo: TextureId, - pub tex_emissive: TextureId, - pub tex_bumpmap: TextureId, -} - -pub struct Mesh { - /// Vertices in the mesh. - pub vertices: Vec, - /// Base UVs. - pub uvs: Vec, - /// Indices in the mesh. - pub indices: Vec, - /// Origin of the mesh. - pub origin: Vec2, -} diff --git a/inox2d/src/node/components/transform_store.rs b/inox2d/src/node/components/transform_store.rs deleted file mode 100644 index 0078065..0000000 --- a/inox2d/src/node/components/transform_store.rs +++ /dev/null @@ -1,10 +0,0 @@ -use glam::Mat4; - -use crate::math::transform::TransformOffset; - -/// Component holding transform values that may be modified across frames. -#[derive(Default, Clone)] -pub struct TransformStore { - pub absolute: Mat4, - pub relative: TransformOffset, -} diff --git a/inox2d/src/node/components/zsort.rs b/inox2d/src/node/components/zsort.rs deleted file mode 100644 index 25e5537..0000000 --- a/inox2d/src/node/components/zsort.rs +++ /dev/null @@ -1,15 +0,0 @@ -use std::ops::Deref; - -/// Component holding zsort values that may be modified across frames. -// only one value instead of absolute + relative as in TransformStore, cause inheritance of zsort (+) is commutative -#[derive(Default)] -pub(crate) struct ZSort(pub f32); - -// so ZSort automatically gets the `.total_cmp()` of `f32` -impl Deref for ZSort { - type Target = f32; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 50db2b3..9ac4827 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -8,7 +8,7 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{deform_stack::DeformSource, DeformStack, TexturedMesh, TransformStore, ZSort}, + components::{DeformSource, DeformStack, TexturedMesh, TransformStore, ZSort}, InoxNodeUuid, }; use crate::puppet::{Puppet, World}; diff --git a/inox2d/src/physics.rs b/inox2d/src/physics.rs index 5466d43..57f8fee 100644 --- a/inox2d/src/physics.rs +++ b/inox2d/src/physics.rs @@ -5,10 +5,7 @@ use std::collections::HashMap; use glam::Vec2; -use crate::node::components::{ - simple_physics::{PhysicsModel, RigidPendulumCtx, SpringPendulumCtx}, - SimplePhysics, TransformStore, -}; +use crate::node::components::{PhysicsModel, RigidPendulumCtx, SimplePhysics, SpringPendulumCtx, TransformStore}; use crate::params::ParamUuid; use crate::puppet::{InoxNodeTree, Puppet, World}; diff --git a/inox2d/src/physics/pendulum.rs b/inox2d/src/physics/pendulum.rs index 4a3b8a7..6228b5a 100644 --- a/inox2d/src/physics/pendulum.rs +++ b/inox2d/src/physics/pendulum.rs @@ -5,7 +5,7 @@ use std::f32::consts::PI; use glam::{Vec2, Vec4}; -use crate::node::components::{simple_physics::ParamMapMode, TransformStore}; +use crate::node::components::{PhysicsParamMapMode, TransformStore}; use super::{SimplePhysicsCtx, SimplePhysicsProps}; @@ -56,13 +56,13 @@ impl SimplePhysicsCtx for T { let relative_length = bob.distance(anchor) / props.1.props.length; let param_value = match props.1.map_mode { - ParamMapMode::XY => { + PhysicsParamMapMode::XY => { let local_pos_norm = local_angle * relative_length; let mut result = local_pos_norm - Vec2::Y; result.y = -result.y; // Y goes up for params result } - ParamMapMode::AngleLength => { + PhysicsParamMapMode::AngleLength => { let a = f32::atan2(-local_angle.x, local_angle.y) / PI; Vec2::new(a, relative_length) } diff --git a/inox2d/src/physics/pendulum/rigid.rs b/inox2d/src/physics/pendulum/rigid.rs index 96d2e96..e23b694 100644 --- a/inox2d/src/physics/pendulum/rigid.rs +++ b/inox2d/src/physics/pendulum/rigid.rs @@ -1,6 +1,6 @@ use glam::{vec2, Vec2}; -use crate::node::components::simple_physics::{PhysicsProps, RigidPendulumCtx}; +use crate::node::components::{PhysicsProps, RigidPendulumCtx}; use crate::physics::{ pendulum::Pendulum, runge_kutta::{IsPhysicsVars, PhysicsState}, diff --git a/inox2d/src/physics/pendulum/spring.rs b/inox2d/src/physics/pendulum/spring.rs index 62b8c2b..bb46934 100644 --- a/inox2d/src/physics/pendulum/spring.rs +++ b/inox2d/src/physics/pendulum/spring.rs @@ -2,7 +2,7 @@ use std::f32::consts::PI; use glam::{vec2, Vec2}; -use crate::node::components::simple_physics::{PhysicsProps, SpringPendulumCtx}; +use crate::node::components::{PhysicsProps, SpringPendulumCtx}; use crate::physics::{ pendulum::Pendulum, runge_kutta::{IsPhysicsVars, PhysicsState}, diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 1937976..4769323 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,13 +1,11 @@ +mod deform_stack; mod vertex_buffers; use std::mem::swap; use crate::model::Model; use crate::node::{ - components::{ - drawable::{Mask, Masks}, - DeformStack, ZSort, - }, + components::{DeformStack, Mask, Masks, ZSort}, drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, InoxNodeUuid, }; diff --git a/inox2d/src/node/components/deform_stack.rs b/inox2d/src/render/deform_stack.rs similarity index 65% rename from inox2d/src/node/components/deform_stack.rs rename to inox2d/src/render/deform_stack.rs index ed8e466..037a23d 100644 --- a/inox2d/src/node/components/deform_stack.rs +++ b/inox2d/src/render/deform_stack.rs @@ -4,30 +4,9 @@ use std::mem::swap; use glam::Vec2; use crate::math::deform::{linear_combine, Deform}; -use crate::node::InoxNodeUuid; -use crate::params::ParamUuid; +use crate::node::components::{DeformSource, DeformStack}; use crate::puppet::{InoxNodeTree, World}; -/// Source of a deform. -#[derive(Hash, PartialEq, Eq, Copy, Clone)] -#[allow(unused)] -pub(crate) enum DeformSource { - Param(ParamUuid), - Node(InoxNodeUuid), -} - -/// Storing deforms specified by multiple sources to apply on one node for one frame. -/// -/// Despite the name (this is respecting the ref impl), this is not in any way a stack. -/// The order of deforms being applied, or more generally speaking, the way multiple deforms adds up to be a single one, needs to be defined according to the spec. -pub(crate) struct DeformStack { - /// this is a component so cannot use generics for the length. - deform_len: usize, - /// map of (src, (enabled, Deform)). - /// On reset, only set enabled to false instead of clearing the map, as deforms from same sources tend to come in every frame. - stack: HashMap, -} - impl DeformStack { pub(crate) fn new(deform_len: usize) -> Self { Self { diff --git a/inox2d/src/render/vertex_buffers.rs b/inox2d/src/render/vertex_buffers.rs index 3d77d74..ca20fa0 100644 --- a/inox2d/src/render/vertex_buffers.rs +++ b/inox2d/src/render/vertex_buffers.rs @@ -3,7 +3,7 @@ use std::slice::from_raw_parts; use glam::{vec2, Vec2}; -use crate::node::components::textured_mesh::Mesh; +use crate::node::components::Mesh; pub struct VertexBuffers { pub verts: Vec, From 912782d4dde708aa4e28aa2e88c453fd7a75057a Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 4 Aug 2024 08:53:37 +0800 Subject: [PATCH 26/34] More direct `VertexBuffers` access. In response to https://github.com/Inochi2D/inox2d/pull/86#discussion_r1698693637 --- inox2d-opengl/src/gl_buffer.rs | 15 ++++++--------- inox2d-opengl/src/lib.rs | 12 +++++++----- inox2d/src/render.rs | 19 +------------------ inox2d/src/render/vertex_buffers.rs | 9 --------- 4 files changed, 14 insertions(+), 41 deletions(-) diff --git a/inox2d-opengl/src/gl_buffer.rs b/inox2d-opengl/src/gl_buffer.rs index f52968a..4edd73c 100644 --- a/inox2d-opengl/src/gl_buffer.rs +++ b/inox2d-opengl/src/gl_buffer.rs @@ -1,3 +1,4 @@ +use glam::Vec2; use glow::HasContext; use super::OpenglRendererError; @@ -14,15 +15,11 @@ unsafe fn upload_array_to_gl(gl: &glow::Context, array: &[T], target: u32, us /// # Errors /// /// This function will return an error if it couldn't create a vertex array. -/// -/// # Safety -/// -/// No prerequisites. pub unsafe fn setup_gl_buffers( gl: &glow::Context, - verts: &[f32], - uvs: &[f32], - deforms: &[f32], + verts: &[Vec2], + uvs: &[Vec2], + deforms: &[Vec2], indices: &[u16], ) -> Result { let vao = gl.create_vertex_array().map_err(OpenglRendererError::Opengl)?; @@ -51,8 +48,8 @@ pub unsafe fn setup_gl_buffers( /// # Safety /// /// The vertex array object created in `setup_gl_buffers()` must be bound and no new ARRAY_BUFFER is enabled. -pub unsafe fn upload_deforms_to_gl(gl: &glow::Context, deforms: &[f32]) { +pub unsafe fn upload_deforms_to_gl(gl: &glow::Context, deforms: &[Vec2]) { // if the above preconditions are met, deform is then the currently bound ARRAY_BUFFER. - let bytes: &[u8] = core::slice::from_raw_parts(deforms.as_ptr() as *const u8, core::mem::size_of_val(deforms)); + let bytes: &[u8] = core::slice::from_raw_parts(deforms.as_ptr() as *const u8, std::mem::size_of_val(deforms)); gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, bytes); } diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 27cd07d..da067cb 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -356,10 +356,10 @@ impl InoxRenderer for OpenglRenderer { let vao = unsafe { setup_gl_buffers( &gl, - inox_buffers.get_raw_verts(), - inox_buffers.get_raw_uvs(), - inox_buffers.get_raw_deforms(), - inox_buffers.get_raw_indices(), + inox_buffers.vertex_buffers.verts.as_slice(), + inox_buffers.vertex_buffers.uvs.as_slice(), + inox_buffers.vertex_buffers.deforms.as_slice(), + inox_buffers.vertex_buffers.indices.as_slice(), ) }?; @@ -604,7 +604,9 @@ impl InoxRenderer for OpenglRenderer { .render_ctx .as_ref() .expect("Rendering for a puppet must be initialized by now.") - .get_raw_deforms(), + .vertex_buffers + .deforms + .as_slice(), ); gl.enable(glow::BLEND); gl.disable(glow::DEPTH_TEST); diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 4769323..53a5a6f 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -33,7 +33,7 @@ pub struct CompositeRenderCtx { /// Additional struct attached to a puppet for rendering. pub struct RenderCtx { /// General compact data buffers for interfacing with the GPU. - vertex_buffers: VertexBuffers, + pub vertex_buffers: VertexBuffers, /// All nodes that need respective draw method calls: /// - including standalone parts and composite parents, /// - excluding (TODO: plain mesh masks) and composite children. @@ -193,23 +193,6 @@ impl RenderCtx { .zip(root_drawable_uuid_zsort_vec.iter()) .for_each(|(old, new)| *old = new.0); } - - /// Memory layout: `[[x, y], [x, y], ...]` - pub fn get_raw_verts(&self) -> &[f32] { - VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.verts) - } - /// Memory layout: `[[x, y], [x, y], ...]` - pub fn get_raw_uvs(&self) -> &[f32] { - VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.uvs) - } - /// Memory layout: `[[i0, i1, i2], [i0, i1, i2], ...]` - pub fn get_raw_indices(&self) -> &[u16] { - self.vertex_buffers.indices.as_slice() - } - /// Memory layout: `[[dx, dy], [dx, dy], ...]` - pub fn get_raw_deforms(&self) -> &[f32] { - VertexBuffers::vec_vec2_as_vec_f32(&self.vertex_buffers.deforms) - } } /// Same as the reference Inochi2D implementation, Inox2D also aims for a "bring your own rendering backend" design. diff --git a/inox2d/src/render/vertex_buffers.rs b/inox2d/src/render/vertex_buffers.rs index ca20fa0..71153ac 100644 --- a/inox2d/src/render/vertex_buffers.rs +++ b/inox2d/src/render/vertex_buffers.rs @@ -1,6 +1,3 @@ -use std::mem::transmute; -use std::slice::from_raw_parts; - use glam::{vec2, Vec2}; use crate::node::components::Mesh; @@ -64,10 +61,4 @@ impl VertexBuffers { (index_offset, vert_offset) } - - pub fn vec_vec2_as_vec_f32(vector: &[Vec2]) -> &[f32] { - let data_ptr = vector.as_ptr(); - // Safety: data of Vec is aligned to 64 and densely packed with f32 - unsafe { from_raw_parts(transmute::<*const Vec2, *const f32>(data_ptr), vector.len() * 2) } - } } From 76b03d2092ab4fd0d77a4005b19f827b35f04a3a Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 4 Aug 2024 09:11:45 +0800 Subject: [PATCH 27/34] Better safety specifications related to GL buffer operations. In response to https://github.com/Inochi2D/inox2d/pull/86#discussion_r1698729703 Plus more robust `upload_array_to_gl()`, with more annotations inside. --- inox2d-opengl/src/gl_buffer.rs | 59 +++++++++++++++++++++++----------- inox2d-opengl/src/lib.rs | 16 ++++----- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/inox2d-opengl/src/gl_buffer.rs b/inox2d-opengl/src/gl_buffer.rs index 4edd73c..6318d2f 100644 --- a/inox2d-opengl/src/gl_buffer.rs +++ b/inox2d-opengl/src/gl_buffer.rs @@ -3,11 +3,30 @@ use glow::HasContext; use super::OpenglRendererError; -unsafe fn upload_array_to_gl(gl: &glow::Context, array: &[T], target: u32, usage: u32) { +/// Create and BIND an OpenGL buffer and upload data. +/// +/// # Errors +/// +/// This function will return an error if it couldn't create a buffer. +/// +/// # Safety +/// +/// `target` and `usage` must be valid OpenGL constants. +unsafe fn upload_array_to_gl( + gl: &glow::Context, + array: &[T], + target: u32, + usage: u32, +) -> Result { + // Safety: + // - array is already a &[T], satisfying all pointer and size requirements. + // - data only accessed immutably in this function, satisfying lifetime requirements. let bytes: &[u8] = core::slice::from_raw_parts(array.as_ptr() as *const u8, std::mem::size_of_val(array)); - let buffer = gl.create_buffer().unwrap(); + let buffer = gl.create_buffer().map_err(OpenglRendererError::Opengl)?; gl.bind_buffer(target, Some(buffer)); gl.buffer_data_u8_slice(target, bytes, usage); + + Ok(buffer) } /// Create a vertex array. Initialize vertex, uv, deform and index buffers, upload content and attach them to the vertex array. Return the array. @@ -15,32 +34,35 @@ unsafe fn upload_array_to_gl(gl: &glow::Context, array: &[T], target: u32, us /// # Errors /// /// This function will return an error if it couldn't create a vertex array. -pub unsafe fn setup_gl_buffers( +pub fn setup_gl_buffers( gl: &glow::Context, verts: &[Vec2], uvs: &[Vec2], deforms: &[Vec2], indices: &[u16], ) -> Result { - let vao = gl.create_vertex_array().map_err(OpenglRendererError::Opengl)?; - gl.bind_vertex_array(Some(vao)); + unsafe { + let vao = gl.create_vertex_array().map_err(OpenglRendererError::Opengl)?; + gl.bind_vertex_array(Some(vao)); + + upload_array_to_gl(gl, verts, glow::ARRAY_BUFFER, glow::STATIC_DRAW)?; + gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(0); - upload_array_to_gl(gl, verts, glow::ARRAY_BUFFER, glow::STATIC_DRAW); - gl.vertex_attrib_pointer_f32(0, 2, glow::FLOAT, false, 0, 0); - gl.enable_vertex_attrib_array(0); + upload_array_to_gl(gl, uvs, glow::ARRAY_BUFFER, glow::STATIC_DRAW)?; + gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(1); - upload_array_to_gl(gl, uvs, glow::ARRAY_BUFFER, glow::STATIC_DRAW); - gl.vertex_attrib_pointer_f32(1, 2, glow::FLOAT, false, 0, 0); - gl.enable_vertex_attrib_array(1); + upload_array_to_gl(gl, deforms, glow::ARRAY_BUFFER, glow::DYNAMIC_DRAW)?; + gl.vertex_attrib_pointer_f32(2, 2, glow::FLOAT, false, 0, 0); + gl.enable_vertex_attrib_array(2); - upload_array_to_gl(gl, deforms, glow::ARRAY_BUFFER, glow::DYNAMIC_DRAW); - gl.vertex_attrib_pointer_f32(2, 2, glow::FLOAT, false, 0, 0); - gl.enable_vertex_attrib_array(2); + upload_array_to_gl(gl, indices, glow::ELEMENT_ARRAY_BUFFER, glow::STATIC_DRAW)?; - upload_array_to_gl(gl, indices, glow::ELEMENT_ARRAY_BUFFER, glow::STATIC_DRAW); + gl.bind_vertex_array(None); - gl.bind_vertex_array(None); - Ok(vao) + Ok(vao) + } } /// Upload full deform buffer content. @@ -49,7 +71,8 @@ pub unsafe fn setup_gl_buffers( /// /// The vertex array object created in `setup_gl_buffers()` must be bound and no new ARRAY_BUFFER is enabled. pub unsafe fn upload_deforms_to_gl(gl: &glow::Context, deforms: &[Vec2]) { - // if the above preconditions are met, deform is then the currently bound ARRAY_BUFFER. + // Safety: same as those described in upload_array_to_gl(). let bytes: &[u8] = core::slice::from_raw_parts(deforms.as_ptr() as *const u8, std::mem::size_of_val(deforms)); + // if the above preconditions are met, deform is then the currently bound ARRAY_BUFFER. gl.buffer_sub_data_u8_slice(glow::ARRAY_BUFFER, 0, bytes); } diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index da067cb..631b32b 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -353,15 +353,13 @@ impl InoxRenderer for OpenglRenderer { .render_ctx .as_ref() .expect("Rendering for a puppet must be initialized before creating a renderer."); - let vao = unsafe { - setup_gl_buffers( - &gl, - inox_buffers.vertex_buffers.verts.as_slice(), - inox_buffers.vertex_buffers.uvs.as_slice(), - inox_buffers.vertex_buffers.deforms.as_slice(), - inox_buffers.vertex_buffers.indices.as_slice(), - ) - }?; + let vao = setup_gl_buffers( + &gl, + inox_buffers.vertex_buffers.verts.as_slice(), + inox_buffers.vertex_buffers.uvs.as_slice(), + inox_buffers.vertex_buffers.deforms.as_slice(), + inox_buffers.vertex_buffers.indices.as_slice(), + )?; // decode textures in parallel let shalltexs = decode_model_textures(model.textures.iter()); From e6e399cb571aeb86166ad7caf7145d9b129438b9 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 4 Aug 2024 09:28:20 +0800 Subject: [PATCH 28/34] Remove `new()` from `trait InoxRenderer`. In response to https://github.com/Inochi2D/inox2d/pull/86#discussion_r1698697131 ...and why `Sized` is required in the first place? --- examples/render-opengl/src/main.rs | 1 - inox2d-opengl/src/lib.rs | 13 +++++++------ inox2d/src/render.rs | 14 +------------- 3 files changed, 8 insertions(+), 20 deletions(-) diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index 436c506..8eea1ed 100644 --- a/examples/render-opengl/src/main.rs +++ b/examples/render-opengl/src/main.rs @@ -3,7 +3,6 @@ use std::{error::Error, fs}; use inox2d::formats::inp::parse_inp; use inox2d::model::Model; -use inox2d::render::InoxRenderer; use inox2d_opengl::OpenglRenderer; use clap::Parser; diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 631b32b..2001c3a 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -318,13 +318,12 @@ impl OpenglRenderer { self.gl.clear(glow::COLOR_BUFFER_BIT); } } -} - -impl InoxRenderer for OpenglRenderer { - type NewParams = glow::Context; - type Error = OpenglRendererError; - fn new(gl: Self::NewParams, model: &Model) -> Result { + /// Given a Model, create an OpenglRenderer: + /// - Setup buffers and shaders. + /// - Decode textures. + /// - Upload static buffer data and textures. + pub fn new(gl: glow::Context, model: &Model) -> Result { // Initialize framebuffers let composite_framebuffer; let cf_albedo; @@ -401,7 +400,9 @@ impl InoxRenderer for OpenglRenderer { Ok(renderer) } +} +impl InoxRenderer for OpenglRenderer { fn on_begin_masks(&self, masks: &Masks) { let gl = &self.gl; diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 53a5a6f..395b047 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -3,7 +3,6 @@ mod vertex_buffers; use std::mem::swap; -use crate::model::Model; use crate::node::{ components::{DeformStack, Mask, Masks, ZSort}, drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, @@ -204,18 +203,7 @@ impl RenderCtx { /// - The renderer may be a debug/just-for-fun renderer intercepting draw calls for other purposes. /// /// Either way, the point is Inox2D will implement a `draw()` method for any `impl InoxRenderer`, dispatching calls based on puppet structure according to Inochi2D standard. -pub trait InoxRenderer -where - Self: Sized, -{ - type NewParams; - type Error; - - /// Create a renderer for one model. - /// - /// Ref impl: Upload textures. - fn new(params: Self::NewParams, model: &Model) -> Result; - +pub trait InoxRenderer { /// Begin masking. /// /// Ref impl: Clear and start writing to the stencil buffer, lock the color buffer. From 599090a196b0d41bf1273c9795488293bb9cc686 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 4 Aug 2024 09:50:54 +0800 Subject: [PATCH 29/34] Better `on_begin_draw()` `on_end_draw()` `draw()` interfaces. In response to https://github.com/Inochi2D/inox2d/pull/86#discussion_r1698705645 The core crate does not need to know about the presence of stuff to do before and after `draw()`. This is up to renderer implementations. --- examples/render-opengl/src/main.rs | 5 ++++- inox2d-opengl/src/lib.rs | 8 +++++-- inox2d/src/render.rs | 35 ++++++++++-------------------- 3 files changed, 21 insertions(+), 27 deletions(-) diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index 8eea1ed..3409136 100644 --- a/examples/render-opengl/src/main.rs +++ b/examples/render-opengl/src/main.rs @@ -3,6 +3,7 @@ use std::{error::Error, fs}; use inox2d::formats::inp::parse_inp; use inox2d::model::Model; +use inox2d::render::InoxRendererExt; use inox2d_opengl::OpenglRenderer; use clap::Parser; @@ -134,7 +135,9 @@ impl App for Inox2dOpenglExampleApp { // Just that physics simulation will run for the provided time, which may be big and causes a startup delay. puppet.end_frame(scene_ctrl.dt()); - inox2d::render::draw(renderer, puppet); + renderer.on_begin_draw(puppet); + renderer.draw(puppet); + renderer.on_end_draw(puppet); } fn handle_window_event(&mut self, event: WindowEvent, elwt: &EventLoopWindowTarget<()>) { diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 2001c3a..36d9b4a 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -588,8 +588,11 @@ impl InoxRenderer for OpenglRenderer { gl.draw_elements(glow::TRIANGLES, 6, glow::UNSIGNED_SHORT, 0); } } +} - fn on_begin_draw(&self, puppet: &Puppet) { +impl OpenglRenderer { + /// Update the renderer with latest puppet data. + pub fn on_begin_draw(&self, puppet: &Puppet) { let gl = &self.gl; // TODO: calculate this matrix only once per draw pass. @@ -612,7 +615,8 @@ impl InoxRenderer for OpenglRenderer { } } - fn on_end_draw(&self, _puppet: &Puppet) { + /// Renderer cleaning up after one frame. + pub fn on_end_draw(&self, _puppet: &Puppet) { let gl = &self.gl; unsafe { diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 395b047..126f4e3 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -249,16 +249,9 @@ pub trait InoxRenderer { render_ctx: &CompositeRenderCtx, id: InoxNodeUuid, ); - - /// Things to do before one pass of drawing a puppet. - /// - /// Ref impl: Upload deform buffer content. - fn on_begin_draw(&self, puppet: &Puppet); - /// Things to do after one pass of drawing a puppet. - fn on_end_draw(&self, puppet: &Puppet); } -trait InoxRendererCommon { +pub trait InoxRendererExt { /// Draw a Drawable, which is potentially masked. fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid); @@ -272,7 +265,7 @@ trait InoxRendererCommon { fn draw(&self, puppet: &Puppet); } -impl InoxRendererCommon for T { +impl InoxRendererExt for T { fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid) { let drawable_kind = DrawableKind::new(id, comps).expect("Node must be a Drawable."); let masks = match drawable_kind { @@ -327,6 +320,15 @@ impl InoxRendererCommon for T { self.finish_composite_content(as_mask, components, render_ctx, id); } + /// Dispatches draw calls for all nodes of `puppet` + /// - with provided renderer implementation, + /// - in Inochi2D standard defined order. + /// + /// This does not guarantee the display of a puppet on screen due to these possible reasons: + /// - Only provided `InoxRenderer` method implementations are called. + /// For example, maybe the caller still need to transfer content from a texture buffer to the screen surface buffer. + /// - The provided `InoxRender` implementation is wrong. + /// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. fn draw(&self, puppet: &Puppet) { for uuid in &puppet .render_ctx @@ -338,18 +340,3 @@ impl InoxRendererCommon for T { } } } - -/// Dispatches draw calls for all nodes of `puppet` -/// - with provided renderer implementation, -/// - in Inochi2D standard defined order. -/// -/// This does not guarantee the display of a puppet on screen due to these possible reasons: -/// - Only provided `InoxRenderer` method implementations are called. -/// For example, maybe the caller still need to transfer content from a texture buffer to the screen surface buffer. -/// - The provided `InoxRender` implementation is wrong. -/// - `puppet` here does not belong to the `model` this `renderer` is initialized with. This will likely result in panics for non-existent node uuids. -pub fn draw(renderer: &T, puppet: &Puppet) { - renderer.on_begin_draw(puppet); - renderer.draw(puppet); - renderer.on_end_draw(puppet); -} From aff042e9b2c84580cd9aaf4187bc1daa6735517f Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 4 Aug 2024 10:37:10 +0800 Subject: [PATCH 30/34] `tracing::warn!()` instead of `panic!()` on non-standard component combinations for a Drawable. In response to https://github.com/Inochi2D/inox2d/pull/86#discussion_r1700035650 --- inox2d/src/node/drawables.rs | 33 +++++++++++++++++++++++++++------ inox2d/src/render.rs | 19 +++++++++++-------- 2 files changed, 38 insertions(+), 14 deletions(-) diff --git a/inox2d/src/node/drawables.rs b/inox2d/src/node/drawables.rs index db6bd57..dbc8869 100644 --- a/inox2d/src/node/drawables.rs +++ b/inox2d/src/node/drawables.rs @@ -33,10 +33,11 @@ pub struct CompositeComponents<'comps> { } impl<'comps> DrawableKind<'comps> { - /// Tries to construct a renderable node data pack from the World of components: - /// - `None` if node not renderable. - /// - Panicks if component combinations invalid. - pub(crate) fn new(id: InoxNodeUuid, comps: &'comps World) -> Option { + /// Tries to construct a renderable node data pack from the World of components. + /// `None` if node not renderable. + /// + /// If `check`, will send a warning to `tracing` if component combination non-standard for a supposed-to-be Drawable node. + pub(crate) fn new(id: InoxNodeUuid, comps: &'comps World, check: bool) -> Option { let drawable = match comps.get::(id) { Some(drawable) => drawable, None => return None, @@ -49,8 +50,28 @@ impl<'comps> DrawableKind<'comps> { let composite = comps.get::(id); match (textured_mesh.is_some(), composite.is_some()) { - (true, true) => panic!("The drawable has both TexturedMesh and Composite."), - (false, false) => panic!("The drawable has neither TexturedMesh nor Composite."), + (true, true) => { + if check { + tracing::warn!( + "Node {} as a Drawable has both TexturedMesh and Composite, treat as TexturedMesh.", + id.0 + ); + } + Some(DrawableKind::TexturedMesh(TexturedMeshComponents { + transform, + drawable, + data: textured_mesh.unwrap(), + })) + } + (false, false) => { + if check { + tracing::warn!( + "Node {} as a Drawable has neither TexturedMesh nor Composite, skipping.", + id.0 + ); + } + None + } (true, false) => Some(DrawableKind::TexturedMesh(TexturedMeshComponents { transform, drawable, diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 126f4e3..7259470 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -49,7 +49,7 @@ impl RenderCtx { let mut root_drawables_count: usize = 0; for node in nodes.iter() { - let drawable_kind = DrawableKind::new(node.uuid, comps); + let drawable_kind = DrawableKind::new(node.uuid, comps, true); if let Some(drawable_kind) = drawable_kind { root_drawables_count += 1; @@ -75,7 +75,7 @@ impl RenderCtx { let children_list: Vec = nodes .get_children(node.uuid) .filter_map(|n| { - if DrawableKind::new(n.uuid, comps).is_some() { + if DrawableKind::new(n.uuid, comps, false).is_some() { Some(n.uuid) } else { None @@ -111,7 +111,7 @@ impl RenderCtx { /// Reset all `DeformStack`. pub(crate) fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { for node in nodes.iter() { - if let Some(DrawableKind::TexturedMesh(..)) = DrawableKind::new(node.uuid, comps) { + if let Some(DrawableKind::TexturedMesh(..)) = DrawableKind::new(node.uuid, comps, false) { let deform_stack = comps .get_mut::(node.uuid) .expect("A TexturedMesh must have an associated DeformStack."); @@ -129,11 +129,14 @@ impl RenderCtx { // root is definitely not a drawable. for node in nodes.iter().skip(1) { - if let Some(drawable_kind) = DrawableKind::new(node.uuid, comps) { + if let Some(drawable_kind) = DrawableKind::new(node.uuid, comps, false) { let parent = nodes.get_parent(node.uuid); let node_zsort = comps.get::(node.uuid).unwrap().0; - if !matches!(DrawableKind::new(parent.uuid, comps), Some(DrawableKind::Composite(_))) { + if !matches!( + DrawableKind::new(parent.uuid, comps, false), + Some(DrawableKind::Composite(_)) + ) { // exclude composite children root_drawable_uuid_zsort_vec.push((node.uuid, node_zsort)); } @@ -267,7 +270,7 @@ pub trait InoxRendererExt { impl InoxRendererExt for T { fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid) { - let drawable_kind = DrawableKind::new(id, comps).expect("Node must be a Drawable."); + let drawable_kind = DrawableKind::new(id, comps, false).expect("Node must be a Drawable."); let masks = match drawable_kind { DrawableKind::TexturedMesh(ref components) => &components.drawable.masks, DrawableKind::Composite(ref components) => &components.drawable.masks, @@ -307,8 +310,8 @@ impl InoxRendererExt for T { self.begin_composite_content(as_mask, components, render_ctx, id); for uuid in &render_ctx.zsorted_children_list { - let drawable_kind = - DrawableKind::new(*uuid, comps).expect("All children in zsorted_children_list should be a Drawable."); + let drawable_kind = DrawableKind::new(*uuid, comps, false) + .expect("All children in zsorted_children_list should be a Drawable."); match drawable_kind { DrawableKind::TexturedMesh(components) => { self.draw_textured_mesh_content(as_mask, &components, comps.get(*uuid).unwrap(), *uuid) From 69549f93378ac84bbb67ee18e82ff4429c315e34 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Sun, 4 Aug 2024 11:09:39 +0800 Subject: [PATCH 31/34] Do not give `TexturedMesh` that is not going to be deformed by any source a `DeformStack`. Thus accordingly, fewer `DeformStack` resetting and reading will happen. Optimization inspired by https://github.com/Inochi2D/inox2d/pull/86#discussion_r1698688185 --- inox2d/src/render.rs | 46 ++++++++++++++++++++++++++++---------------- 1 file changed, 29 insertions(+), 17 deletions(-) diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 7259470..6f460d5 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,6 +1,7 @@ mod deform_stack; mod vertex_buffers; +use std::collections::HashSet; use std::mem::swap; use crate::node::{ @@ -8,6 +9,7 @@ use crate::node::{ drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, InoxNodeUuid, }; +use crate::params::BindingValues; use crate::puppet::{InoxNodeTree, Puppet, World}; pub use vertex_buffers::VertexBuffers; @@ -45,6 +47,16 @@ impl RenderCtx { let nodes = &puppet.nodes; let comps = &mut puppet.node_comps; + let mut nodes_to_deform = HashSet::new(); + for param in &puppet.params { + param.1.bindings.iter().for_each(|b| { + if matches!(b.values, BindingValues::Deform(_)) { + nodes_to_deform.insert(b.node); + } + }); + } + // TODO: Further fill the set when Meshgroup is implemented. + let mut vertex_buffers = VertexBuffers::default(); let mut root_drawables_count: usize = 0; @@ -68,7 +80,11 @@ impl RenderCtx { vert_len, }, ); - comps.add(node.uuid, DeformStack::new(vert_len)); + + // TexturedMesh not deformed by any source does not need a DeformStack + if nodes_to_deform.contains(&node.uuid) { + comps.add(node.uuid, DeformStack::new(vert_len)); + } } DrawableKind::Composite { .. } => { // exclude non-drawable children @@ -111,10 +127,7 @@ impl RenderCtx { /// Reset all `DeformStack`. pub(crate) fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { for node in nodes.iter() { - if let Some(DrawableKind::TexturedMesh(..)) = DrawableKind::new(node.uuid, comps, false) { - let deform_stack = comps - .get_mut::(node.uuid) - .expect("A TexturedMesh must have an associated DeformStack."); + if let Some(deform_stack) = comps.get_mut::(node.uuid) { deform_stack.reset(); } } @@ -172,18 +185,17 @@ impl RenderCtx { } // for TexturedMesh, obtain and write deforms into vertex_buffer DrawableKind::TexturedMesh(..) => { - let render_ctx = comps.get::(node.uuid).unwrap(); - let vert_offset = render_ctx.vert_offset as usize; - let vert_len = render_ctx.vert_len; - let deform_stack = comps - .get::(node.uuid) - .expect("A TexturedMesh must have an associated DeformStack."); - - deform_stack.combine( - nodes, - comps, - &mut self.vertex_buffers.deforms[vert_offset..(vert_offset + vert_len)], - ); + // A TexturedMesh not having an associated DeformStack means it will not be deformed at all, skip. + if let Some(deform_stack) = comps.get::(node.uuid) { + let render_ctx = comps.get::(node.uuid).unwrap(); + let vert_offset = render_ctx.vert_offset as usize; + let vert_len = render_ctx.vert_len; + deform_stack.combine( + nodes, + comps, + &mut self.vertex_buffers.deforms[vert_offset..(vert_offset + vert_len)], + ); + } } } } From 7f4c9c07bfc96d1d7aea778d413d401d9c328da4 Mon Sep 17 00:00:00 2001 From: Ruiqi Niu Date: Thu, 8 Aug 2024 09:16:15 +0800 Subject: [PATCH 32/34] Generalize `Mesh` into a standalone component. For future meshgroup implementation. --- inox2d-opengl/src/lib.rs | 2 +- inox2d/src/formats/payload.rs | 3 ++- inox2d/src/node/components.rs | 6 ++++-- inox2d/src/node/drawables.rs | 15 +++++++++++---- inox2d/src/params.rs | 13 ++++++------- inox2d/src/render.rs | 5 ++--- 6 files changed, 26 insertions(+), 18 deletions(-) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index 36d9b4a..a5b5419 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -474,7 +474,7 @@ impl InoxRenderer for OpenglRenderer { glDisableVertexAttribArray(0); */ - self.bind_part_textures(components.data); + self.bind_part_textures(components.texture); self.set_blend_mode(components.drawable.blending.mode); let mvp = self.camera.matrix(self.viewport.as_vec2()) * *components.transform; diff --git a/inox2d/src/formats/payload.rs b/inox2d/src/formats/payload.rs index 7bf48b8..dc8d041 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -139,7 +139,6 @@ fn deserialize_textured_mesh(obj: JsonObject) -> InoxParseResult { }; Ok(TexturedMesh { - mesh: vals("mesh", deserialize_mesh(obj.get_object("mesh")?))?, tex_albedo, tex_emissive, tex_bumpmap, @@ -324,6 +323,8 @@ impl Puppet { "Part" => { self.node_comps.add(id, deserialize_drawable(data)?); self.node_comps.add(id, deserialize_textured_mesh(data)?); + self.node_comps + .add(id, vals("mesh", deserialize_mesh(data.get_object("mesh")?))?) } "Composite" => { self.node_comps.add(id, deserialize_drawable(data)?); diff --git a/inox2d/src/node/components.rs b/inox2d/src/node/components.rs index 8110780..7d1c252 100644 --- a/inox2d/src/node/components.rs +++ b/inox2d/src/node/components.rs @@ -1,7 +1,7 @@ /*! Inochi2D node types to Inox2D components: - Node -> (Nothing) -- Part -> Drawable + TexturedMesh +- Part -> Drawable + TexturedMesh + Mesh - Composite -> Drawable + Composite - SimplePhysics -> SimplePhysics - Custom nodes by inheritance -> Custom nodes by composition @@ -177,12 +177,14 @@ pub(crate) struct SpringPendulumCtx { /// If has this as a component, the node should render a deformed texture pub struct TexturedMesh { - pub mesh: Mesh, pub tex_albedo: TextureId, pub tex_emissive: TextureId, pub tex_bumpmap: TextureId, } +/* --- MESH --- */ + +/// A deformable mesh, deforming either textures (TexturedMesh nodes), or children (MeshGroup nodes) pub struct Mesh { /// Vertices in the mesh. pub vertices: Vec, diff --git a/inox2d/src/node/drawables.rs b/inox2d/src/node/drawables.rs index dbc8869..451f09b 100644 --- a/inox2d/src/node/drawables.rs +++ b/inox2d/src/node/drawables.rs @@ -1,7 +1,7 @@ use glam::Mat4; use crate::node::{ - components::{Composite, Drawable, TexturedMesh, TransformStore}, + components::{Composite, Drawable, Mesh, TexturedMesh, TransformStore}, InoxNodeUuid, }; use crate::puppet::World; @@ -21,7 +21,8 @@ pub struct TexturedMeshComponents<'comps> { // Only the absolute part of `TransformStore` that the renderer backend may need. pub transform: &'comps Mat4, pub drawable: &'comps Drawable, - pub data: &'comps TexturedMesh, + pub texture: &'comps TexturedMesh, + pub mesh: &'comps Mesh, } /// Pack of components for a Composite node. @@ -60,7 +61,10 @@ impl<'comps> DrawableKind<'comps> { Some(DrawableKind::TexturedMesh(TexturedMeshComponents { transform, drawable, - data: textured_mesh.unwrap(), + texture: textured_mesh.unwrap(), + mesh: comps + .get::(id) + .expect("A TexturedMesh must have an associated Mesh."), })) } (false, false) => { @@ -75,7 +79,10 @@ impl<'comps> DrawableKind<'comps> { (true, false) => Some(DrawableKind::TexturedMesh(TexturedMeshComponents { transform, drawable, - data: textured_mesh.unwrap(), + texture: textured_mesh.unwrap(), + mesh: comps + .get::(id) + .expect("A TexturedMesh must have an associated Mesh."), })), (false, true) => Some(DrawableKind::Composite(CompositeComponents { transform, diff --git a/inox2d/src/params.rs b/inox2d/src/params.rs index 9ac4827..e7a0733 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -8,7 +8,7 @@ use crate::math::{ matrix::Matrix2d, }; use crate::node::{ - components::{DeformSource, DeformStack, TexturedMesh, TransformStore, ZSort}, + components::{DeformSource, DeformStack, Mesh, TransformStore, ZSort}, InoxNodeUuid, }; use crate::puppet::{Puppet, World}; @@ -186,9 +186,11 @@ impl Param { // deform specified by a parameter must be direct, i.e., in the form of displacements of all vertices let direct_deform = { - let textured_mesh = comps.get::(binding.node); - if let Some(textured_mesh) = textured_mesh { - let vert_len = textured_mesh.mesh.vertices.len(); + let mesh = comps + .get::(binding.node) + .expect("Deform param target must have an associated Mesh."); + + let vert_len = mesh.vertices.len(); let mut direct_deform: Vec = Vec::with_capacity(vert_len); direct_deform.resize(vert_len, Vec2::ZERO); @@ -202,9 +204,6 @@ impl Param { ); direct_deform - } else { - todo!("Deform on node types other than Part.") - } }; comps diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 6f460d5..d806b40 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -67,9 +67,8 @@ impl RenderCtx { match drawable_kind { DrawableKind::TexturedMesh(components) => { - let (index_offset, vert_offset) = vertex_buffers.push(&components.data.mesh); - let (index_len, vert_len) = - (components.data.mesh.indices.len(), components.data.mesh.vertices.len()); + let (index_offset, vert_offset) = vertex_buffers.push(components.mesh); + let (index_len, vert_len) = (components.mesh.indices.len(), components.mesh.vertices.len()); comps.add( node.uuid, From 45f6a3640c00a2c80c78845755dad38faa56b14c Mon Sep 17 00:00:00 2001 From: Speykious Date: Sun, 11 Aug 2024 17:26:24 +0200 Subject: [PATCH 33/34] Move `OpenglRenderer::new` up in impl block --- inox2d-opengl/src/lib.rs | 164 +++++++++++++++++++-------------------- 1 file changed, 82 insertions(+), 82 deletions(-) diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index a5b5419..19382c6 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -134,6 +134,88 @@ pub struct OpenglRenderer { } impl OpenglRenderer { + /// Given a Model, create an OpenglRenderer: + /// - Setup buffers and shaders. + /// - Decode textures. + /// - Upload static buffer data and textures. + pub fn new(gl: glow::Context, model: &Model) -> Result { + // Initialize framebuffers + let composite_framebuffer; + let cf_albedo; + let cf_emissive; + let cf_bump; + let cf_stencil; + unsafe { + cf_albedo = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + cf_emissive = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + cf_bump = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + cf_stencil = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + + composite_framebuffer = gl.create_framebuffer().map_err(OpenglRendererError::Opengl)?; + } + + // Shaders + let part_shader = PartShader::new(&gl)?; + let part_mask_shader = PartMaskShader::new(&gl)?; + let composite_shader = CompositeShader::new(&gl)?; + let composite_mask_shader = CompositeMaskShader::new(&gl)?; + + let support_debug_extension = gl.supported_extensions().contains("GL_KHR_debug"); + + let inox_buffers = model + .puppet + .render_ctx + .as_ref() + .expect("Rendering for a puppet must be initialized before creating a renderer."); + let vao = setup_gl_buffers( + &gl, + inox_buffers.vertex_buffers.verts.as_slice(), + inox_buffers.vertex_buffers.uvs.as_slice(), + inox_buffers.vertex_buffers.deforms.as_slice(), + inox_buffers.vertex_buffers.indices.as_slice(), + )?; + + // decode textures in parallel + let shalltexs = decode_model_textures(model.textures.iter()); + let textures = shalltexs + .iter() + .enumerate() + .map(|e| { + tracing::debug!("Uploading shallow texture {:?}", e.0); + texture::Texture::from_shallow_texture(&gl, e.1).map_err(|e| OpenglRendererError::Opengl(e.to_string())) + }) + .collect::, _>>()?; + + let renderer = Self { + gl, + support_debug_extension, + camera: Camera::default(), + viewport: UVec2::default(), + cache: RefCell::new(GlCache::default()), + + vao, + + composite_framebuffer, + cf_albedo, + cf_emissive, + cf_bump, + cf_stencil, + + part_shader, + part_mask_shader, + composite_shader, + composite_mask_shader, + + textures, + }; + + // Set emission strength once (it doesn't change anywhere else) + renderer.bind_shader(&renderer.part_shader); + renderer.part_shader.set_emission_strength(&renderer.gl, 1.); + + Ok(renderer) + } + /// Pushes an OpenGL debug group. /// This is very useful to debug OpenGL calls per node with `apitrace`, as it will nest calls inside of labels, /// making it trivial to know which calls correspond to which nodes. @@ -318,88 +400,6 @@ impl OpenglRenderer { self.gl.clear(glow::COLOR_BUFFER_BIT); } } - - /// Given a Model, create an OpenglRenderer: - /// - Setup buffers and shaders. - /// - Decode textures. - /// - Upload static buffer data and textures. - pub fn new(gl: glow::Context, model: &Model) -> Result { - // Initialize framebuffers - let composite_framebuffer; - let cf_albedo; - let cf_emissive; - let cf_bump; - let cf_stencil; - unsafe { - cf_albedo = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - cf_emissive = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - cf_bump = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - cf_stencil = gl.create_texture().map_err(OpenglRendererError::Opengl)?; - - composite_framebuffer = gl.create_framebuffer().map_err(OpenglRendererError::Opengl)?; - } - - // Shaders - let part_shader = PartShader::new(&gl)?; - let part_mask_shader = PartMaskShader::new(&gl)?; - let composite_shader = CompositeShader::new(&gl)?; - let composite_mask_shader = CompositeMaskShader::new(&gl)?; - - let support_debug_extension = gl.supported_extensions().contains("GL_KHR_debug"); - - let inox_buffers = model - .puppet - .render_ctx - .as_ref() - .expect("Rendering for a puppet must be initialized before creating a renderer."); - let vao = setup_gl_buffers( - &gl, - inox_buffers.vertex_buffers.verts.as_slice(), - inox_buffers.vertex_buffers.uvs.as_slice(), - inox_buffers.vertex_buffers.deforms.as_slice(), - inox_buffers.vertex_buffers.indices.as_slice(), - )?; - - // decode textures in parallel - let shalltexs = decode_model_textures(model.textures.iter()); - let textures = shalltexs - .iter() - .enumerate() - .map(|e| { - tracing::debug!("Uploading shallow texture {:?}", e.0); - texture::Texture::from_shallow_texture(&gl, e.1).map_err(|e| OpenglRendererError::Opengl(e.to_string())) - }) - .collect::, _>>()?; - - let renderer = Self { - gl, - support_debug_extension, - camera: Camera::default(), - viewport: UVec2::default(), - cache: RefCell::new(GlCache::default()), - - vao, - - composite_framebuffer, - cf_albedo, - cf_emissive, - cf_bump, - cf_stencil, - - part_shader, - part_mask_shader, - composite_shader, - composite_mask_shader, - - textures, - }; - - // Set emission strength once (it doesn't change anywhere else) - renderer.bind_shader(&renderer.part_shader); - renderer.part_shader.set_emission_strength(&renderer.gl, 1.); - - Ok(renderer) - } } impl InoxRenderer for OpenglRenderer { From 21136c247971f4a38ecf9bea1dfe9c78b31aa7a0 Mon Sep 17 00:00:00 2001 From: Speykious Date: Tue, 24 Sep 2024 23:31:40 +0200 Subject: [PATCH 34/34] Fix WebGL example --- examples/render-webgl/src/main.rs | 35 +++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/examples/render-webgl/src/main.rs b/examples/render-webgl/src/main.rs index b62e17f..3bc79cf 100644 --- a/examples/render-webgl/src/main.rs +++ b/examples/render-webgl/src/main.rs @@ -41,7 +41,8 @@ async fn run() -> Result<(), Box> { use std::cell::RefCell; use std::rc::Rc; - use inox2d::{formats::inp::parse_inp, render::InoxRenderer}; + use inox2d::formats::inp::parse_inp; + use inox2d::render::InoxRendererExt; use inox2d_opengl::OpenglRenderer; use glam::Vec2; @@ -86,13 +87,18 @@ async fn run() -> Result<(), Box> { .await?; let model_bytes = res.bytes().await?; - let model = parse_inp(model_bytes.as_ref())?; + let mut model = parse_inp(model_bytes.as_ref())?; + + tracing::info!("Setting up puppet for transforms, params and rendering."); + model.puppet.init_transforms(); + model.puppet.init_rendering(); + model.puppet.init_params(); + model.puppet.init_physics(); info!("Initializing Inox2D renderer"); - let mut renderer = OpenglRenderer::new(gl)?; + let mut renderer = OpenglRenderer::new(gl, &model)?; info!("Creating buffers and uploading model textures"); - renderer.prepare(&model)?; renderer.camera.scale = Vec2::splat(0.15); info!("Inox2D renderer initialized"); @@ -115,15 +121,26 @@ async fn run() -> Result<(), Box> { *anim_loop_g.borrow_mut() = Some(Closure::new(move || { scene_ctrl.borrow_mut().update(&mut renderer.borrow_mut().camera); - renderer.borrow().clear(); { + renderer.borrow().clear(); + let mut puppet = puppet.borrow_mut(); - puppet.begin_set_params(); + puppet.begin_frame(); let t = scene_ctrl.borrow().current_elapsed(); - puppet.set_named_param("Head:: Yaw-Pitch", Vec2::new(t.cos(), t.sin())); - puppet.end_set_params(scene_ctrl.borrow().dt()); + let _ = puppet + .param_ctx + .as_mut() + .unwrap() + .set("Head:: Yaw-Pitch", Vec2::new(t.cos(), t.sin())); + + // Actually, not providing 0 for the first frame will not create too big a problem. + // Just that physics simulation will run for the provided time, which may be big and causes a startup delay. + puppet.end_frame(scene_ctrl.borrow().dt()); + + renderer.borrow().on_begin_draw(&puppet); + renderer.borrow().draw(&puppet); + renderer.borrow().on_end_draw(&puppet); } - renderer.borrow().render(&puppet.borrow()); request_animation_frame(anim_loop_f.borrow().as_ref().unwrap()); }));