diff --git a/examples/render-opengl/src/main.rs b/examples/render-opengl/src/main.rs index f8b8282..3409136 100644 --- a/examples/render-opengl/src/main.rs +++ b/examples/render-opengl/src/main.rs @@ -3,7 +3,7 @@ use std::{error::Error, fs}; use inox2d::formats::inp::parse_inp; use inox2d::model::Model; -use inox2d::render::InoxRenderer; +use inox2d::render::InoxRendererExt; use inox2d_opengl::OpenglRenderer; use clap::Parser; @@ -41,12 +41,18 @@ 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(); + model.puppet.init_physics(); + tracing::info!("Setting up windowing and OpenGL"); let app_frame = AppFrame::init( WindowBuilder::new() @@ -81,10 +87,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 +124,20 @@ 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())); + // 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()); + + 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/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()); })); diff --git a/inox2d-opengl/src/gl_buffer.rs b/inox2d-opengl/src/gl_buffer.rs index 6b7ba8d..4a0ad47 100644 --- a/inox2d-opengl/src/gl_buffer.rs +++ b/inox2d-opengl/src/gl_buffer.rs @@ -1,85 +1,44 @@ +use glam::Vec2; 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) -> glow::Buffer { +/// 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. +pub 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); - buffer -} - -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, buffer: glow::Buffer); + Ok(buffer) } -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); - - let deform_buffer = - 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(deform_buffer) - } - - /// # 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, buffer: glow::Buffer) { - gl.bind_buffer(glow::ARRAY_BUFFER, Some(buffer)); - - reupload_array_to_gl( - gl, - &self.vertex_buffers.deforms, - glow::ARRAY_BUFFER, - 0, - self.vertex_buffers.deforms.len(), - ); - } -} +/// Upload full deform buffer content. +/// +/// # 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: &[Vec2], buffer: glow::Buffer) { + gl.bind_buffer(glow::ARRAY_BUFFER, Some(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); +} \ No newline at end of file diff --git a/inox2d-opengl/src/lib.rs b/inox2d-opengl/src/lib.rs index fe504c5..a165990 100644 --- a/inox2d-opengl/src/lib.rs +++ b/inox2d-opengl/src/lib.rs @@ -7,20 +7,25 @@ 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::{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::{upload_array_to_gl, upload_deforms_to_gl}; #[derive(Debug, thiserror::Error)] #[error("Could not initialize OpenGL renderer: {0}")] @@ -29,8 +34,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, @@ -109,7 +114,6 @@ impl GlCache { } } -#[allow(unused)] pub struct OpenglRenderer { gl: glow::Context, support_debug_extension: bool, @@ -118,6 +122,7 @@ pub struct OpenglRenderer { cache: RefCell, vao: glow::VertexArray, + deform_buffer: glow::Buffer, composite_framebuffer: glow::Framebuffer, cf_albedo: glow::Texture, @@ -129,83 +134,100 @@ pub struct OpenglRenderer { part_mask_shader: PartMaskShader, composite_shader: CompositeShader, composite_mask_shader: CompositeMaskShader, - deform_buffer: Option, 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; + /// 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 { 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)?; + // Initialize framebuffers + let cf_albedo = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + let cf_emissive = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + let cf_bump = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + let cf_stencil = gl.create_texture().map_err(OpenglRendererError::Opengl)?; + + let 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."); + + // setup GL buffers + let vao = gl.create_vertex_array().map_err(OpenglRendererError::Opengl)?; + gl.bind_vertex_array(Some(vao)); + + let verts = inox_buffers.vertex_buffers.verts.as_slice(); + 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); + + let uvs = inox_buffers.vertex_buffers.uvs.as_slice(); + 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); + + let deforms = inox_buffers.vertex_buffers.deforms.as_slice(); + let deform_buffer = 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); + + let indices = inox_buffers.vertex_buffers.indices.as_slice(); + upload_array_to_gl(&gl, indices, glow::ELEMENT_ARRAY_BUFFER, glow::STATIC_DRAW)?; + + gl.bind_vertex_array(None); + + // 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, + deform_buffer, + + 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) } - - // 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(), - deform_buffer: None, - }; - - // 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. @@ -255,7 +277,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; } @@ -304,7 +326,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; } @@ -317,7 +339,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; } @@ -356,21 +378,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> { - self.deform_buffer = Some(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; @@ -400,67 +409,38 @@ impl InoxRenderer for OpenglRenderer { self.update_camera(); } - fn clear(&self) { + pub fn clear(&self) { self.cache.borrow_mut().clear(); 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!() - } - - fn render(&self, puppet: &Puppet) { +impl InoxRenderer for OpenglRenderer { + fn on_begin_masks(&self, masks: &Masks) { let gl = &self.gl; - unsafe { - if let Some(deform_buffer) = self.deform_buffer { - puppet.render_ctx.upload_deforms_to_gl(gl, deform_buffer); - } - - gl.enable(glow::BLEND); - gl.disable(glow::DEPTH_TEST); - } - let camera = self - .camera - .matrix(Vec2::new(self.viewport.x as f32, self.viewport.y as f32)); - self.draw(&camera, puppet); - } - - fn on_end_scene(&self) { - todo!() - } - - fn draw_scene(&self) { - todo!() - } - - fn on_begin_mask(&self, has_mask: bool) { - 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); } } @@ -483,9 +463,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); @@ -502,36 +489,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.texture); + 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); @@ -540,23 +510,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; @@ -577,7 +552,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(); @@ -585,7 +566,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(); @@ -606,11 +587,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); @@ -624,3 +605,39 @@ impl InoxRenderer for OpenglRenderer { } } } + +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. + // 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.") + .vertex_buffers + .deforms + .as_slice(), + self.deform_buffer, + ); + gl.enable(glow::BLEND); + gl.disable(glow::DEPTH_TEST); + } + } + + /// Renderer cleaning up after one frame. + pub fn on_end_draw(&self, _puppet: &Puppet) { + let gl = &self.gl; + + unsafe { + gl.bind_vertex_array(None); + } + } +} 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..dc8d041 100644 --- a/inox2d/src/formats/payload.rs +++ b/inox2d/src/formats/payload.rs @@ -1,27 +1,19 @@ 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::*; 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::physics::PuppetPhysics; +use crate::puppet::{meta::*, Puppet}; use crate::texture::TextureId; +use super::f32s_as_vec2s; use super::json::{JsonError, JsonObject, SerialExtend}; pub type InoxParseResult = Result; @@ -56,6 +48,8 @@ pub enum InoxParseError { Not2FloatsInList(usize), } +// json structure helpers + impl InoxParseError { pub fn nested(self, key: &str) -> Self { match self { @@ -76,41 +70,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,32 +138,25 @@ 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 { 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")? { - "AngleLength" => ParamMapMode::AngleLength, - "XY" => ParamMapMode::XY, + "AngleLength" => PhysicsParamMapMode::AngleLength, + "XY" => PhysicsParamMapMode::XY, unknown => return Err(InoxParseError::UnknownParamMapMode(unknown.to_owned())), }, @@ -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,114 @@ 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)?); + self.node_comps + .add(id, vals("mesh", deserialize_mesh(data.get_object("mesh")?))?) + } + "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 +397,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 +474,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 +500,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..987f3e7 100644 --- a/inox2d/src/lib.rs +++ b/inox2d/src/lib.rs @@ -1,6 +1,5 @@ pub mod formats; pub mod math; -pub mod mesh; pub mod model; pub mod node; pub mod params; 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/math/transform.rs b/inox2d/src/math/transform.rs index 33ff865..a46ad17 100644 --- a/inox2d/src/math/transform.rs +++ b/inox2d/src/math/transform.rs @@ -1,6 +1,7 @@ use glam::{EulerRot, Mat4, Quat, Vec2, Vec3}; -#[derive(Debug, Clone, Copy)] +/// relative transform +#[derive(Debug, Clone)] pub struct TransformOffset { /// X Y Z pub translation: Vec3, 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..432f3dc 100644 --- a/inox2d/src/node.rs +++ b/inox2d/src/node.rs @@ -1,49 +1,17 @@ -pub mod data; -pub mod tree; - -use std::fmt::Debug; +pub mod components; +pub mod drawables; 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..7d1c252 --- /dev/null +++ b/inox2d/src/node/components.rs @@ -0,0 +1,247 @@ +/*! +Inochi2D node types to Inox2D components: +- Node -> (Nothing) +- Part -> Drawable + TexturedMesh + Mesh +- Composite -> Drawable + Composite +- SimplePhysics -> SimplePhysics +- Custom nodes by inheritance -> Custom nodes by composition +*/ + +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 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, + /// 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. +/// 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. +#[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/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/drawables.rs b/inox2d/src/node/drawables.rs new file mode 100644 index 0000000..451f09b --- /dev/null +++ b/inox2d/src/node/drawables.rs @@ -0,0 +1,94 @@ +use glam::Mat4; + +use crate::node::{ + components::{Composite, Drawable, Mesh, TexturedMesh, TransformStore}, + 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> { + // Only the absolute part of `TransformStore` that the renderer backend may need. + pub transform: &'comps Mat4, + pub drawable: &'comps Drawable, + pub texture: &'comps TexturedMesh, + pub mesh: &'comps Mesh, +} + +/// Pack of components for a Composite node. +pub struct CompositeComponents<'comps> { + // Only the absolute part of `TransformStore` that the renderer backend may need. + pub transform: &'comps Mat4, + 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. + /// + /// 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, + }; + 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); + + match (textured_mesh.is_some(), composite.is_some()) { + (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, + texture: textured_mesh.unwrap(), + mesh: comps + .get::(id) + .expect("A TexturedMesh must have an associated Mesh."), + })) + } + (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, + texture: textured_mesh.unwrap(), + mesh: comps + .get::(id) + .expect("A TexturedMesh must have an associated Mesh."), + })), + (false, true) => Some(DrawableKind::Composite(CompositeComponents { + transform, + drawable, + data: composite.unwrap(), + })), + } + } +} 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 ad1bfce..a7727ae 100644 --- a/inox2d/src/params.rs +++ b/inox2d/src/params.rs @@ -1,13 +1,19 @@ +use std::collections::HashMap; + 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::puppet::Puppet; -use crate::render::{NodeRenderCtxs, PartRenderCtx, RenderCtxKind}; +use crate::math::{ + deform::Deform, + interp::{bi_interpolate_f32, bi_interpolate_vec2s_additive, InterpRange, InterpolateMode}, + matrix::Matrix2d, +}; +use crate::node::{ + components::{DeformSource, DeformStack, Mesh, TransformStore, ZSort}, + InoxNodeUuid, +}; +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. -#[derive(Debug, Clone)] pub struct Binding { pub node: InoxNodeUuid, pub is_set: Matrix2d, @@ -50,7 +56,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, @@ -63,7 +68,12 @@ pub struct Param { } impl Param { - pub fn apply(&self, val: Vec2, node_render_ctxs: &mut NodeRenderCtxs, deform_buf: &mut [Vec2]) { + /// 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, comps: &mut World) { let val = val.clamp(self.min, self.max); let val_normed = (val - self.min) / (self.max - self.min); @@ -96,8 +106,6 @@ impl Param { // Apply offset on each binding for binding in &self.bindings { - let node_offsets = node_render_ctxs.get_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]), @@ -106,51 +114,73 @@ impl Param { let val_normed = val_normed.clamp(range_in.beg, range_in.end); 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); + + 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); + 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); + 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 *= + 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 *= + 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); + 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); + 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); + 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( @@ -162,92 +192,82 @@ 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 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); + + bi_interpolate_vec2s_additive( + val_normed, + range_in, + out_top, + out_bottom, + binding.interpolate_mode, + &mut direct_deform, + ); + + direct_deform + }; + + comps + .get_mut::(binding.node) + .expect("Nodes being deformed must have a DeformStack component.") + .push(DeformSource::Param(self.uuid), Deform::Direct(direct_deform)); } } } } } -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)?) - } +/// Additional struct attached to a puppet for animating through params. +pub 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(crate) 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; + /// Reset all params to default value. + pub(crate) fn reset(&mut self, params: &HashMap) { + for (name, value) in self.values.iter_mut() { + *value = 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(&mut self, param_name: &str, val: Vec2) -> Result<(), SetParamError> { + if let Some(value) = self.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(&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() { + // TODO: a correct implementation should not fail on param value (0, 0) + if *val != Vec2::ZERO { + 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}")] 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/physics.rs b/inox2d/src/physics.rs index 40e1878..57f8fee 100644 --- a/inox2d/src/physics.rs +++ b/inox2d/src/physics.rs @@ -1,106 +1,138 @@ pub mod pendulum; pub(crate) mod runge_kutta; -use std::f32::consts::PI; +use std::collections::HashMap; -use glam::{vec2, vec4, Vec2}; +use glam::Vec2; -use crate::node::data::{InoxData, ParamMapMode, PhysicsModel, SimplePhysics}; -use crate::puppet::{Puppet, PuppetPhysics}; -use crate::render::NodeRenderCtx; +use crate::node::components::{PhysicsModel, RigidPendulumCtx, SimplePhysics, SpringPendulumCtx, TransformStore}; +use crate::params::ParamUuid; +use crate::puppet::{InoxNodeTree, Puppet, World}; -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) + self.calc_output(props, transform, anchor) } +} - 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), - }; - } +/// 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, +} - 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), - }; +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()), + } + } + } - vec2(anchor.x, anchor.y) + Self { + t: 0., + param_uuid_to_name: puppet.params.iter().map(|p| (p.1.uuid, p.0.to_owned())).collect(), + } } - 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; + 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."); + } - 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) + 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; - param_value * oscale + values_to_apply } } diff --git a/inox2d/src/physics/pendulum.rs b/inox2d/src/physics/pendulum.rs index 150771f..6228b5a 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::{PhysicsParamMapMode, 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 { + 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 + } + PhysicsParamMapMode::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..e23b694 100644 --- a/inox2d/src/physics/pendulum/rigid.rs +++ b/inox2d/src/physics/pendulum/rigid.rs @@ -1,12 +1,15 @@ 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::{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. +#[derive(Default)] +pub(crate) struct RigidPendulum { pub θ: f32, pub ω: f32, } @@ -21,34 +24,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..bb46934 100644 --- a/inox2d/src/physics/pendulum/spring.rs +++ b/inox2d/src/physics/pendulum/spring.rs @@ -1,32 +1,19 @@ -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::{PhysicsProps, SpringPendulumCtx}; +use crate::physics::{ + pendulum::Pendulum, + runge_kutta::{IsPhysicsVars, PhysicsState}, + PuppetPhysics, SimplePhysicsProps, +}; - self.vars.bob_pos - } +/// Variables for Runge-Kutta method. +#[derive(Default)] +pub(crate) struct SpringPendulum { + pub bob_pos: Vec2, + pub bob_vel: Vec2, } impl IsPhysicsVars<4> for SpringPendulum { @@ -39,10 +26,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 +61,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..a99ec74 100644 --- a/inox2d/src/physics/runge_kutta.rs +++ b/inox2d/src/physics/runge_kutta.rs @@ -1,66 +1,62 @@ -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 { +#[derive(Default)] +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 af256fd..ce67ff8 100644 --- a/inox2d/src/puppet.rs +++ b/inox2d/src/puppet.rs @@ -1,300 +1,170 @@ -#![allow(dead_code)] +pub mod meta; +mod transforms; +mod tree; +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::params::{Param, ParamUuid}; +use crate::node::{InoxNode, InoxNodeUuid}; +use crate::params::{Param, ParamCtx}; +use crate::physics::{PhysicsCtx, PuppetPhysics}; 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, -} +use meta::PuppetMeta; +use transforms::TransformCtx; +pub use tree::InoxNodeTree; +pub use world::World; -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", - } - ) +/// Inochi2D puppet. +pub struct Puppet { + pub meta: PuppetMeta, + physics: PuppetPhysics, + physics_ctx: Option, + pub(crate) nodes: InoxNodeTree, + pub(crate) node_comps: World, + /// 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, + /// Context for animating puppet with parameters. See `.init_params()` + pub param_ctx: Option, +} + +impl Puppet { + pub(crate) fn new( + meta: PuppetMeta, + physics: PuppetPhysics, + root: InoxNode, + params: HashMap, + ) -> Self { + Self { + meta, + physics, + physics_ctx: None, + nodes: InoxNodeTree::new_with_root(root), + node_comps: World::new(), + transform_ctx: None, + render_ctx: None, + params, + param_ctx: None, + } } -} -/// 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, -} + /// 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.") + } -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", - } - ) + let transform_ctx = TransformCtx::new(self); + self.transform_ctx = Some(transform_ctx); } -} -/// 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, -} + /// 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."); + } -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", - } - ) + let render_ctx = RenderCtx::new(self); + self.render_ctx = Some(render_ctx); } -} -/// 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" - } -} + /// 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."); + } -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" - } - ) + let param_ctx = ParamCtx::new(self); + self.param_ctx = Some(param_ctx); } -} -/// 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, -} + /// 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."); + } -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}")?; + let physics_ctx = PhysicsCtx::new(self); + self.physics_ctx = Some(physics_ctx); } - 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}")? - } + /// 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); } - 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}")?; + if let Some(transform_ctx) = self.transform_ctx.as_mut() { + transform_ctx.reset(&self.nodes, &mut self.node_comps); } - 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(), + if let Some(param_ctx) = self.param_ctx.as_mut() { + param_ctx.reset(&self.params); } } -} - -/// Global physics parameters for the puppet. -#[derive(Clone, Copy, Debug)] -pub struct PuppetPhysics { - pub pixels_per_meter: f32, - pub gravity: f32, -} -/// Inochi2D puppet. -#[derive(Clone, Debug)] -pub struct Puppet { - pub meta: PuppetMeta, - pub physics: PuppetPhysics, - pub nodes: InoxNodeTree, - pub drivers: Vec, - 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); + /// 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); + } - let drivers = (nodes.arena.iter()) - .filter_map(|node| { - let node = node.get(); + if let Some(transform_ctx) = self.transform_ctx.as_mut() { + transform_ctx.update(&self.nodes, &mut self.node_comps); + } - match node.data { - InoxData::SimplePhysics(_) => Some(node.uuid), - _ => None, - } - }) - .collect::>(); + 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); - 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); + transform_ctx.update(&self.nodes, &mut self.node_comps); } - Self { - meta, - physics, - nodes, - drivers, - params, - param_names, - render_ctx, + 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/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/transforms.rs b/inox2d/src/puppet/transforms.rs new file mode 100644 index 0000000..ac11a15 --- /dev/null +++ b/inox2d/src/puppet/transforms.rs @@ -0,0 +1,56 @@ +use crate::node::components::{TransformStore, ZSort}; + +use super::{InoxNodeTree, Puppet, World}; + +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.clone(); + comps.get_mut::(node.uuid).unwrap().0 = node.zsort; + } + } + + /// 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(&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; + + 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 = 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().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.0 * node_trans; + + let node_zsort = comps.get_mut::(node.uuid).unwrap(); + node_zsort.0 += base.1; + } + } +} diff --git a/inox2d/src/puppet/tree.rs b/inox2d/src/puppet/tree.rs new file mode 100644 index 0000000..4adf19c --- /dev/null +++ b/inox2d/src/puppet/tree.rs @@ -0,0 +1,96 @@ +use std::collections::HashMap; + +use indextree::Arena; + +use crate::node::{InoxNode, InoxNodeUuid}; + +pub struct InoxNodeTree { + // 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, +} + +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_node_id: 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()) + } + + /// 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()) + } +} diff --git a/inox2d/src/puppet/world.rs b/inox2d/src/puppet/world.rs new file mode 100644 index 0000000..ebad444 --- /dev/null +++ b/inox2d/src/puppet/world.rs @@ -0,0 +1,259 @@ +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, 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, + 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 + // provenance considerations present, see comment for VecBytes + 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); + // 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 struct World { + // Type -> (Column, Ownership) + columns: HashMap)>, +} + +pub trait Component: 'static + Send + Sync {} +impl Component for T {} + +impl World { + pub fn new() -> Self { + Self { + columns: HashMap::new(), + } + } + + /// 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) + } +} + +impl Default for World { + fn default() -> Self { + Self::new() + } +} + +#[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' }); + } + } + } + + 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 safe_ops() { + let mut world = World::new(); + + assert!(world.get::(NODE_0).is_none()); + + 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 }); + + assert!(world.get::(NODE_0).is_some()); + assert!(world.get::(NODE_1).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); + } + } + } +} diff --git a/inox2d/src/render.rs b/inox2d/src/render.rs index 376b4b1..d806b40 100644 --- a/inox2d/src/render.rs +++ b/inox2d/src/render.rs @@ -1,378 +1,356 @@ -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::tree::InoxNodeTree; -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, -} - -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) - } -} - -#[derive(Debug, Clone)] -pub struct PartRenderCtx { +mod deform_stack; +mod vertex_buffers; + +use std::collections::HashSet; +use std::mem::swap; + +use crate::node::{ + components::{DeformStack, Mask, Masks, ZSort}, + drawables::{CompositeComponents, DrawableKind, TexturedMeshComponents}, + InoxNodeUuid, +}; +use crate::params::BindingValues; +use crate::puppet::{InoxNodeTree, Puppet, World}; + +pub use vertex_buffers::VertexBuffers; + +/// 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, pub index_len: usize, pub vert_len: usize, } -#[derive(Debug, Clone)] -pub enum RenderCtxKind { - Node, - Part(PartRenderCtx), - Composite(Vec), +/// Additional info per node for rendering a Composite. +pub struct CompositeRenderCtx { + pub zsorted_children_list: Vec, } -#[derive(Clone, Debug)] -pub struct NodeRenderCtx { - pub trans: Mat4, - pub trans_offset: TransformOffset, - pub kind: RenderCtxKind, -} - -pub type NodeRenderCtxs = HashMap; - -#[derive(Clone, Debug)] +/// Additional struct attached to a puppet for rendering. pub struct RenderCtx { + /// General compact data buffers for interfacing with the GPU. pub 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, + /// - excluding (TODO: plain mesh masks) and composite children. + root_drawables_zsorted: Vec, } impl RenderCtx { - pub fn new(nodes: &InoxNodeTree) -> 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 { - 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, - }, - }, - ); + /// MODIFIES puppet. In addition to initializing self, installs render contexts in the World of components + pub(super) fn new(puppet: &mut Puppet) -> Self { + 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. - for uuid in nodes.zsorted_root() { - let node = nodes.get_node(uuid).unwrap(); + let mut vertex_buffers = VertexBuffers::default(); - match node.data { - InoxData::Part(_) | InoxData::Composite(_) => { - root_drawables_zsorted.push(uuid); - } + let mut root_drawables_count: usize = 0; + for node in nodes.iter() { + let drawable_kind = DrawableKind::new(node.uuid, comps, true); + if let Some(drawable_kind) = drawable_kind { + root_drawables_count += 1; + + match drawable_kind { + DrawableKind::TexturedMesh(components) => { + 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, + TexturedMeshRenderCtx { + index_offset, + vert_offset, + index_len, + 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 + let children_list: Vec = nodes + .get_children(node.uuid) + .filter_map(|n| { + if DrawableKind::new(n.uuid, comps, false).is_some() { + Some(n.uuid) + } else { + None + } + }) + .collect(); + + // composite children are excluded from root_drawables_zsorted + root_drawables_count -= children_list.len(); + + comps.add( + node.uuid, + CompositeRenderCtx { + // sort later, before render + zsorted_children_list: children_list, + }, + ); + } + }; } } + let mut root_drawables_zsorted = Vec::new(); + // similarly, populate later, before render + root_drawables_zsorted.resize(root_drawables_count, InoxNodeUuid(0)); + Self { vertex_buffers, root_drawables_zsorted, - 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(); + /// Reset all `DeformStack`. + pub(crate) fn reset(&mut self, nodes: &InoxNodeTree, comps: &mut World) { + for node in nodes.iter() { + if let Some(deform_stack) = comps.get_mut::(node.uuid) { + deform_stack.reset(); } } } -} - -pub trait InoxRenderer -where - Self: Sized, -{ - type Error; - /// For any model-specific setup, e.g. creating buffers with specific sizes. - /// - /// After this step, the provided model should be renderable. - fn prepare(&mut self, model: &Model) -> Result<(), Self::Error>; - - /// Resize the renderer's viewport. - fn resize(&mut self, w: u32, h: u32); + /// 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 root_drawable_uuid_zsort_vec = Vec::<(InoxNodeUuid, f32)>::new(); + + // root is definitely not a drawable. + for node in nodes.iter().skip(1) { + 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, false), + Some(DrawableKind::Composite(_)) + ) { + // exclude composite children + root_drawable_uuid_zsort_vec.push((node.uuid, node_zsort)); + } - /// Clear the canvas. - fn clear(&self); + 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(..) => { + // 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)], + ); + } + } + } + } + } - /// 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. - fn draw_scene(&self); + root_drawable_uuid_zsort_vec.sort_by(|a, b| a.1.total_cmp(&b.1).reverse()); + self.root_drawables_zsorted + .iter_mut() + .zip(root_drawable_uuid_zsort_vec.iter()) + .for_each(|(old, new)| *old = new.0); + } +} +/// 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 { /// 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); - /// Read only from the stencil buffer, unlock 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. + /// + /// Ref impl: Read only from the stencil buffer, unlock the color buffer. fn on_begin_masked_content(&self); - /// Disable the stencil buffer. + /// End masking. + /// + /// Ref impl: 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)? + fn draw_textured_mesh_content( &self, as_mask: bool, - camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, + components: &TexturedMeshComponents, + render_ctx: &TexturedMeshRenderCtx, + id: InoxNodeUuid, ); - /// When something needs to happen before drawing to the composite buffers. - fn begin_composite_content(&self); - /// Transfer content from composite buffers to normal buffers. - fn finish_composite_content(&self, as_mask: bool, composite: &Composite); -} - -pub trait InoxRendererCommon { - /// Draw one part, with its content properly masked. - fn draw_part( + /// Begin compositing. Get prepared for rendering children of a Composite. + /// + /// Ref impl: Prepare composite buffers. + fn begin_composite_content( &self, - camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, - puppet: &Puppet, + as_mask: bool, + components: &CompositeComponents, + render_ctx: &CompositeRenderCtx, + id: InoxNodeUuid, ); - - /// Draw one composite. - fn draw_composite( + /// End compositing. + /// + /// Ref impl: Transfer content from composite buffers to normal buffers. + fn finish_composite_content( &self, as_mask: bool, - camera: &Mat4, - composite: &Composite, - puppet: &Puppet, - children: &[InoxNodeUuid], + components: &CompositeComponents, + render_ctx: &CompositeRenderCtx, + id: InoxNodeUuid, ); +} + +pub trait InoxRendererExt { + /// Draw a Drawable, which is potentially masked. + 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, id: InoxNodeUuid); /// 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_part( - &self, - camera: &Mat4, - node_render_ctx: &NodeRenderCtx, - part: &Part, - part_render_ctx: &PartRenderCtx, - puppet: &Puppet, - ) { - 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); - } - - (InoxData::Composite(ref mask_composite), RenderCtxKind::Composite(ref mask_children)) => { - self.draw_composite(true, camera, mask_composite, puppet, mask_children); - } - - _ => { - // This match block clearly is sign that the data structure needs rework - todo!(); - } - } +impl InoxRendererExt for T { + fn draw_drawable(&self, as_mask: bool, comps: &World, id: InoxNodeUuid) { + 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, + }; + + 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); + + self.draw_drawable(true, comps, mask.source); } self.on_begin_masked_content(); - self.draw_part_self(false, camera, node_render_ctx, part, part_render_ctx); + } + + match drawable_kind { + DrawableKind::TexturedMesh(ref components) => { + 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 { self.on_end_mask(); - } else { - self.draw_part_self(false, camera, node_render_ctx, part, part_render_ctx); } } - fn draw_composite( - &self, - as_mask: bool, - camera: &Mat4, - comp: &Composite, - puppet: &Puppet, - children: &[InoxNodeUuid], - ) { - if children.is_empty() { + 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(); - - for &uuid in children { - let node = puppet.nodes.get_node(uuid).unwrap(); - let node_render_ctx = &puppet.render_ctx.node_render_ctxs[&uuid]; + self.begin_composite_content(as_mask, components, render_ctx, id); - 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); + for uuid in &render_ctx.zsorted_children_list { + 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) } - } else { - // composite inside composite simply cannot happen + DrawableKind::Composite { .. } => panic!("Composite inside Composite not allowed."), } } - self.finish_composite_content(as_mask, comp); + self.finish_composite_content(as_mask, components, render_ctx, id); } - 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!(); - } - } + /// 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 + .as_ref() + .expect("RenderCtx of puppet must be initialized before calling draw().") + .root_drawables_zsorted + { + self.draw_drawable(false, &puppet.node_comps, *uuid); } } } diff --git a/inox2d/src/render/deform_stack.rs b/inox2d/src/render/deform_stack.rs new file mode 100644 index 0000000..037a23d --- /dev/null +++ b/inox2d/src/render/deform_stack.rs @@ -0,0 +1,61 @@ +use std::collections::HashMap; +use std::mem::swap; + +use glam::Vec2; + +use crate::math::deform::{linear_combine, Deform}; +use crate::node::components::{DeformSource, DeformStack}; +use crate::puppet::{InoxNodeTree, World}; + +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: 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."); + } + + 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/render/vertex_buffers.rs b/inox2d/src/render/vertex_buffers.rs new file mode 100644 index 0000000..71153ac --- /dev/null +++ b/inox2d/src/render/vertex_buffers.rs @@ -0,0 +1,64 @@ +use glam::{vec2, Vec2}; + +use crate::node::components::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) + } +}