diff --git a/Cargo.lock b/Cargo.lock index 208b0be..b9ace23 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1114,7 +1114,7 @@ dependencies = [ [[package]] name = "flax" version = "0.7.1" -source = "git+https://github.com/ten3roberts/flax#c07e645b60ee3f777edc9fb241d285945b60f784" +source = "git+https://github.com/ten3roberts/flax#ad6ebf0b09dd07b91e24f7175fec32eba9e5bda6" dependencies = [ "anyhow", "atomic_refcell", @@ -1136,7 +1136,7 @@ dependencies = [ [[package]] name = "flax-derive" version = "0.7.1" -source = "git+https://github.com/ten3roberts/flax#c07e645b60ee3f777edc9fb241d285945b60f784" +source = "git+https://github.com/ten3roberts/flax#ad6ebf0b09dd07b91e24f7175fec32eba9e5bda6" dependencies = [ "heck", "itertools 0.13.0", @@ -2216,7 +2216,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4979f22fdb869068da03c9f7528f8297c6fd2606bc3a4affe42e6a823fdb8da4" dependencies = [ "cfg-if", - "windows-targets 0.52.6", + "windows-targets 0.48.5", ] [[package]] diff --git a/examples/instancing.rs b/examples/instancing.rs new file mode 100644 index 0000000..053f2ae --- /dev/null +++ b/examples/instancing.rs @@ -0,0 +1,235 @@ +use std::iter::repeat; + +use flax::{component, BatchSpawn, FetchExt, Query, System, World}; +use glam::{vec3, Mat4, Quat, Vec3}; +use itertools::iproduct; +use ivy_assets::{fs::AssetPath, AssetCache}; +use ivy_core::{ + app::PostInitEvent, + layer::events::EventRegisterContext, + palette::Srgb, + profiling::ProfilingLayer, + update_layer::{FixedTimeStep, Plugin, ScheduledLayer}, + App, Color, ColorExt, EngineLayer, Layer, +}; +use ivy_engine::{ + color, elapsed_time, engine, main_camera, parent_transform, position, rotation, scale, + world_transform, +}; +use ivy_game::free_camera::{setup_camera, FreeCameraPlugin}; +use ivy_gltf::animation::plugin::AnimationPlugin; +use ivy_input::layer::InputLayer; +use ivy_physics::{GizmoSettings, PhysicsPlugin}; +use ivy_postprocessing::preconfigured::{ + pbr::{PbrRenderGraphConfig, SkyboxConfig}, + SurfacePbrPipelineDesc, SurfacePbrRenderer, +}; +use ivy_wgpu::{ + components::{environment_data, forward_pass, projection_matrix, shadow_pass}, + driver::WinitDriver, + events::ResizedEvent, + layer::GraphicsLayer, + material_desc::{MaterialData, PbrMaterialData}, + mesh_desc::MeshDesc, + primitives::{CubePrimitive, UvSpherePrimitive}, + renderer::EnvironmentData, +}; +use tracing_subscriber::{layer::SubscriberExt, registry, util::SubscriberInitExt, EnvFilter}; +use tracing_tree::HierarchicalLayer; +use wgpu::TextureFormat; +use winit::{dpi::LogicalSize, window::WindowAttributes}; + +const ENABLE_SKYBOX: bool = true; + +pub fn main() -> anyhow::Result<()> { + registry() + .with(EnvFilter::from_default_env()) + .with( + HierarchicalLayer::default() + .with_indent_lines(true) + .with_deferred_spans(true) + .with_span_retrace(true), + ) + .init(); + + if let Err(err) = App::builder() + .with_driver(WinitDriver::new( + WindowAttributes::default() + .with_inner_size(LogicalSize::new(1920, 1080)) + .with_title("Ivy"), + )) + .with_layer(EngineLayer::new()) + .with_layer(ProfilingLayer::new()) + .with_layer(GraphicsLayer::new(|world, assets, store, gpu, surface| { + Ok(SurfacePbrRenderer::new( + world, + assets, + store, + gpu, + surface, + SurfacePbrPipelineDesc { + pbr_config: PbrRenderGraphConfig { + label: "basic".into(), + shadow_map_config: Some(Default::default()), + msaa: Some(Default::default()), + bloom: Some(Default::default()), + skybox: Some(SkyboxConfig { + hdri: Box::new(AssetPath::new("hdris/EveningSkyHDRI035B_8K-HDR.exr")), + format: TextureFormat::Rgba16Float, + }), + hdr_format: Some(wgpu::TextureFormat::Rgba16Float), + }, + ..Default::default() + }, + )) + })) + .with_layer(InputLayer::new()) + .with_layer(LogicLayer::new()) + .with_layer( + ScheduledLayer::new(FixedTimeStep::new(0.02)) + .with_plugin(FreeCameraPlugin) + .with_plugin(AnimationPlugin) + .with_plugin(DynamicsPlugin) + .with_plugin( + PhysicsPlugin::new() + .with_gravity(Vec3::ZERO) + .with_gizmos(GizmoSettings { rigidbody: true }), + ), + ) + .run() + { + tracing::error!("{err:?}"); + Err(err) + } else { + Ok(()) + } +} + +pub struct LogicLayer {} + +impl Default for LogicLayer { + fn default() -> Self { + Self::new() + } +} + +impl LogicLayer { + pub fn new() -> Self { + Self {} + } + + fn setup_objects(&mut self, world: &mut World, assets: &AssetCache) -> anyhow::Result<()> { + let sphere_mesh = MeshDesc::content(assets.load(&UvSpherePrimitive::default())); + + let plastic_material = MaterialData::PbrMaterial( + PbrMaterialData::new() + .with_metallic_factor(1.0) + .with_roughness_factor(0.4), + ); + + let sidelength = 100; + let spacing = 25.0; + let positions = iproduct!(0..sidelength, 0..sidelength, 0..sidelength) + .map(|(x, y, z)| vec3(x as f32 * spacing, y as f32 * spacing, z as f32 * spacing)); + + let transforms = iproduct!(0..sidelength, 0..sidelength, 0..sidelength).map(|(x, y, z)| { + Mat4::from_translation(vec3( + x as f32 * spacing, + y as f32 * spacing, + z as f32 * spacing, + )) + }); + + let mut builder = BatchSpawn::new(sidelength * sidelength * sidelength); + builder.set(position(), positions)?; + builder.set(rotation(), repeat(Quat::IDENTITY))?; + builder.set(scale(), repeat(Vec3::ONE))?; + builder.set(world_transform(), transforms)?; + builder.set(parent_transform(), repeat(Mat4::IDENTITY))?; + builder.set(rotate_target(), repeat(()))?; + builder.set(ivy_wgpu::components::mesh(), repeat(sphere_mesh))?; + builder.set(color(), repeat(Color::white()))?; + builder.set(forward_pass(), repeat(plastic_material))?; + builder.set(shadow_pass(), repeat(MaterialData::ShadowMaterial))?; + + tracing::info!("spawning {} objects", builder.len()); + builder.spawn(world); + tracing::info!("finished"); + + Ok(()) + } +} + +impl Layer for LogicLayer { + fn register( + &mut self, + world: &mut World, + _: &AssetCache, + mut events: EventRegisterContext, + ) -> anyhow::Result<()> { + events.subscribe(|this, ctx, _: &PostInitEvent| this.setup_objects(ctx.world, ctx.assets)); + + events.subscribe(|_, ctx, resized: &ResizedEvent| { + if let Some(main_camera) = Query::new(projection_matrix().as_mut()) + .with(main_camera()) + .borrow(ctx.world) + .first() + { + let aspect = + resized.physical_size.width as f32 / resized.physical_size.height as f32; + tracing::info!(%aspect); + *main_camera = Mat4::perspective_rh(1.0, aspect, 0.1, 5000.0); + } + + Ok(()) + }); + + setup_camera() + .set( + environment_data(), + EnvironmentData::new( + Srgb::new(0.2, 0.2, 0.3), + 0.005, + if ENABLE_SKYBOX { 0.0 } else { 1.0 }, + ), + ) + .spawn(world); + + Ok(()) + } +} + +pub struct DynamicsPlugin; + +component! { + rotate_target: (), +} + +impl Plugin for DynamicsPlugin { + fn install( + &self, + _: &mut World, + _: &AssetCache, + schedules: &mut ivy_core::update_layer::ScheduleSetBuilder, + ) -> anyhow::Result<()> { + let rotate_system = System::builder() + .with_query( + Query::new(( + rotate_target(), + rotation().as_mut(), + elapsed_time().source(engine()), + )) + .batch_size(256), + ) + .par_for_each(|(_, rotation, elapsed)| { + *rotation = + Quat::from_axis_angle(vec3(1.0, 0.2, 0.0).normalize(), elapsed.as_secs_f32()); + }); + // #[system(args(elapsed=elapsed_time().source(engine())), par)] + // fn rotate(rotate_target: &(), rotation: &mut Quat, elapsed: &Duration) {} + + // schedules.per_tick_mut().with_system(rotate_system); + + Ok(()) + } +} diff --git a/ivy-core/src/components/mod.rs b/ivy-core/src/components/mod.rs index 9ab9f94..f1caae3 100644 --- a/ivy-core/src/components/mod.rs +++ b/ivy-core/src/components/mod.rs @@ -35,6 +35,7 @@ flax::component! { pub engine, } +#[cfg(feature = "serde")] flax::register_serializable! { position, rotation, @@ -47,6 +48,7 @@ flax::register_serializable! { } #[derive(Fetch, Debug, Clone)] +#[fetch(transforms=[Modified])] pub struct TransformQuery { pub pos: Component, pub rotation: Component, @@ -92,6 +94,7 @@ impl Default for TransformQuery { } } +#[cfg(feature = "serde")] fn one_scale() -> Vec3 { Vec3::ONE } diff --git a/ivy-core/src/layer/mod.rs b/ivy-core/src/layer/mod.rs index c9118dc..14cc99d 100644 --- a/ivy-core/src/layer/mod.rs +++ b/ivy-core/src/layer/mod.rs @@ -6,7 +6,7 @@ use crate::{ app::TickEvent, components::{async_commandbuffer, engine, gizmos, request_capture_mouse}, gizmos::Gizmos, - systems::{apply_async_commandbuffers, update_transform_system}, + systems::{apply_async_commandbuffers, update_root_transforms_system, update_transform_system}, AsyncCommandBuffer, }; @@ -76,6 +76,7 @@ impl EngineLayer { let cmd = AsyncCommandBuffer::new(); let schedule = Schedule::builder() .with_system(apply_async_commandbuffers(cmd.clone())) + .with_system(update_root_transforms_system()) .with_system(update_transform_system()) .build(); diff --git a/ivy-core/src/systems.rs b/ivy-core/src/systems.rs index cb74d7b..cb9369d 100644 --- a/ivy-core/src/systems.rs +++ b/ivy-core/src/systems.rs @@ -1,12 +1,38 @@ use anyhow::Context; -use flax::{components::child_of, BoxedSystem, Dfs, DfsBorrow, Query, System, World}; +use flax::{ + components::child_of, system, BoxedSystem, Dfs, DfsBorrow, FetchExt, Query, RelationExt, + System, World, +}; use glam::{Mat4, Quat, Vec3}; use crate::{ - components::{position, rotation, scale, world_transform}, + components::{position, rotation, scale, world_transform, TransformQuery}, AsyncCommandBuffer, }; +// #[system(args(position=position(),rotation=rotation().modified(),scale=scale()), par)] +// pub fn update_root_transforms( +// world_transform: &mut Mat4, +// position: &Vec3, +// rotation: &Quat, +// scale: &Vec3, +// ) { +// *world_transform = Mat4::from_scale_rotation_translation(*scale, *rotation, *position) +// } + +pub fn update_root_transforms_system() -> BoxedSystem { + System::builder() + .with_query( + Query::new((world_transform().as_mut(), TransformQuery::new().modified())) + .batch_size(1024), + ) + .par_for_each(|(world_transform, item)| { + *world_transform = + Mat4::from_scale_rotation_translation(*item.scale, *item.rotation, *item.pos) + }) + .boxed() +} + pub fn update_transform_system() -> BoxedSystem { System::builder() .with_query( diff --git a/ivy-gltf/src/animation/player.rs b/ivy-gltf/src/animation/player.rs index cb169bd..aacea3f 100644 --- a/ivy-gltf/src/animation/player.rs +++ b/ivy-gltf/src/animation/player.rs @@ -141,15 +141,20 @@ impl AnimationPlayer { self.progress += step_time * self.speed; // Do this after stepping to not show past-end lerps when we could have wrapped - if self.looping && self.progress > self.animation.duration() { - self.channels.iter_mut().for_each(|v| v.left_keyframe = 0); - self.progress %= self.animation.duration(); - } else if self.looping && self.progress < 0.0 { - self.channels - .iter_mut() - .zip(self.animation.channels()) - .for_each(|(v, channel)| v.left_keyframe = channel.times.len() - 2); - self.progress = (self.progress + self.animation.duration()) % self.animation.duration(); + if self.looping { + if self.progress > self.animation.duration() { + self.channels.iter_mut().for_each(|v| v.left_keyframe = 0); + self.progress %= self.animation.duration(); + } else if self.progress < 0.0 { + self.channels + .iter_mut() + .zip(self.animation.channels()) + .for_each(|(v, channel)| v.left_keyframe = channel.times.len() - 2); + self.progress = + (self.progress + self.animation.duration()) % self.animation.duration(); + } + } else { + self.progress = self.progress.clamp(0.0, self.animation.duration()); } for (i, state) in self.channels.iter_mut().enumerate() { @@ -223,6 +228,10 @@ impl AnimationPlayer { }; } } + + pub fn progress(&self) -> f32 { + self.progress + } } #[derive(Debug)] diff --git a/ivy-gltf/src/animation/skin.rs b/ivy-gltf/src/animation/skin.rs index b03d5ba..37872b0 100644 --- a/ivy-gltf/src/animation/skin.rs +++ b/ivy-gltf/src/animation/skin.rs @@ -1,6 +1,6 @@ use std::{ collections::{BTreeMap, BTreeSet}, - path::PathBuf, + path::{Path, PathBuf}, }; use anyhow::Context; @@ -39,6 +39,7 @@ impl Skin { assets: &AssetCache, document: &gltf::Document, buffer_data: &[buffer::Data], + path: &Path, ) -> anyhow::Result>> { // NOTE: each joint in a skin refers to a node in the scene hierarchy let joint_maps = document @@ -63,7 +64,11 @@ impl Skin { .iter() .position(|v| v.contains_key(&joint_scene_index)) else { - tracing::error!("Missing skin for animation"); + tracing::error!( + "No skin for animation target joint {:?} referenced in animation {:?} in document {path:?}", + target.node().name(), + animation.name(), + ); return; }; @@ -203,9 +208,8 @@ impl Skin { } } -#[derive( - Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Serialize, serde::Deserialize, -)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct SkinDesc { document: PathBuf, node: String, diff --git a/ivy-gltf/src/lib.rs b/ivy-gltf/src/lib.rs index b199c7e..33e2ff8 100644 --- a/ivy-gltf/src/lib.rs +++ b/ivy-gltf/src/lib.rs @@ -92,6 +92,7 @@ impl std::ops::Deref for DocumentData { impl Document { async fn load(assets: &AssetCache, path: impl AsRef) -> anyhow::Result { + let path = path.as_ref(); let bytes: Asset> = assets.from_path(path).await?; let mut gltf = Gltf::from_slice(&bytes)?; @@ -168,7 +169,7 @@ impl Document { .filter_map(|(i, v)| Some((v.name().map(ToString::to_string)?, i))) .collect(); - let skins = Skin::load_from_document(assets, &gltf.document, &buffer_data)?; + let skins = Skin::load_from_document(assets, &gltf.document, &buffer_data, path)?; let data = assets.insert(DocumentData { gltf, diff --git a/ivy-scene/src/lib.rs b/ivy-scene/src/lib.rs index 6784f07..6823e0d 100644 --- a/ivy-scene/src/lib.rs +++ b/ivy-scene/src/lib.rs @@ -4,7 +4,7 @@ use flax::{ components::{child_of, name}, Entity, EntityBuilder, }; -use ivy_core::{components::color, Color, ColorExt, EntityBuilderExt}; +use ivy_core::EntityBuilderExt; use ivy_gltf::{animation::player::Animator, components::animator, GltfNode}; use ivy_wgpu::{ components::{forward_pass, shadow_pass}, @@ -32,62 +32,51 @@ impl GltfNodeExt for GltfNode { entity: &'a mut EntityBuilder, opts: &NodeMountOptions, ) -> &'a mut EntityBuilder { - mount(self, entity, opts, true) - } -} - -fn mount<'a>( - node: &GltfNode, - entity: &'a mut EntityBuilder, - opts: &NodeMountOptions, - root: bool, -) -> &'a mut EntityBuilder { - let skin = node.skin(); - - if let Some(mesh) = node.mesh() { - for primitive in mesh.primitives() { - let gltf_material = primitive.material(); - - let material = gltf_material - .name() - .and_then(|name| opts.material_overrides.get(name).cloned()) - .unwrap_or_else(|| { - MaterialData::PbrMaterial(PbrMaterialData::from_gltf_material(gltf_material)) - }); - - let materials = [ - (forward_pass(), material), - (shadow_pass(), MaterialData::ShadowMaterial), - ]; - - let mut child = Entity::builder(); - - child - .mount(RenderObjectBundle::new(primitive.into(), &materials)) - .set_opt(name(), mesh.name().map(ToOwned::to_owned)); - - entity.attach(child_of, child); + let skin = self.skin(); + + if let Some(mesh) = self.mesh() { + for primitive in mesh.primitives() { + let gltf_material = primitive.material(); + + let material = gltf_material + .name() + .and_then(|name| opts.material_overrides.get(name).cloned()) + .unwrap_or_else(|| { + MaterialData::PbrMaterial(PbrMaterialData::from_gltf_material( + gltf_material, + )) + }); + + let materials = [ + (forward_pass(), material), + (shadow_pass(), MaterialData::ShadowMaterial), + ]; + + let mut child = Entity::builder(); + + child + .mount(RenderObjectBundle::new(primitive.into(), &materials)) + .set_opt(name(), mesh.name().map(ToOwned::to_owned)); + + entity.attach(child_of, child); + } } - } - if let Some(skin) = skin { - entity.set(ivy_gltf::components::skin(), skin); - entity.set(animator(), Animator::new()); - } + if let Some(skin) = skin { + entity.set(ivy_gltf::components::skin(), skin); + entity.set(animator(), Animator::new()); + } - entity.mount(node.transform()); + entity.mount(self.transform()); - if root { - entity.set(color(), Color::white()); - } + for child in self.children() { + if child.children().next().is_none() && child.mesh().is_none() { + continue; + } - for child in node.children() { - if child.children().next().is_none() && child.mesh().is_none() { - continue; + entity.attach(child_of, child.mount(&mut Entity::builder(), opts)); } - entity.attach(child_of, mount(&child, &mut Entity::builder(), opts, false)); + entity } - - entity } diff --git a/ivy-wgpu/src/material_desc.rs b/ivy-wgpu/src/material_desc.rs index 1f33426..22981b0 100644 --- a/ivy-wgpu/src/material_desc.rs +++ b/ivy-wgpu/src/material_desc.rs @@ -48,15 +48,15 @@ impl Load for MaterialDesc { #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct PbrMaterialDesc { label: String, - #[serde(default = "TextureDesc::white")] + #[cfg_attr(feature = "serde", serde(default = "TextureDesc::white"))] albedo: TextureDesc, - #[serde(default = "TextureDesc::default_normal")] + #[cfg_attr(feature = "serde", serde(default = "TextureDesc::default_normal"))] normal: TextureDesc, - #[serde(default = "TextureDesc::white")] + #[cfg_attr(feature = "serde", serde(default = "TextureDesc::white"))] metallic_roughness: TextureDesc, - #[serde(default = "TextureDesc::white")] + #[cfg_attr(feature = "serde", serde(default = "TextureDesc::white"))] ambient_occlusion: TextureDesc, - #[serde(default = "TextureDesc::white")] + #[cfg_attr(feature = "serde", serde(default = "TextureDesc::white"))] displacement: TextureDesc, roughness_factor: NotNan, metallic_factor: NotNan, diff --git a/ivy-wgpu/src/renderer/mesh_renderer.rs b/ivy-wgpu/src/renderer/mesh_renderer.rs index 5531653..fd26891 100644 --- a/ivy-wgpu/src/renderer/mesh_renderer.rs +++ b/ivy-wgpu/src/renderer/mesh_renderer.rs @@ -7,7 +7,7 @@ use bytemuck::{Pod, Zeroable}; use flax::{ entity_ids, fetch::{entity_refs, EntityRefs, Satisfied}, - filter::{All, ChangeFilter, With}, + filter::{All, ChangeFilter}, Component, Entity, EntityIds, FetchExt, Query, World, }; use glam::{vec4, Mat4, Vec3, Vec4, Vec4Swizzles}; @@ -17,7 +17,6 @@ use ivy_core::{profiling::profile_function, subscribers::RemovedComponentSubscri use ivy_wgpu_types::{ multi_buffer::SubBuffer, shader::Culling, BindGroupBuilder, BindGroupLayoutBuilder, }; -use slab::Slab; use wgpu::{BindGroup, BindGroupLayout, CommandEncoder, DepthBiasState, RenderPass, ShaderStages}; use super::{ @@ -68,6 +67,7 @@ impl Batch { pub type ShaderFactory = Box ShaderDesc>; +#[derive(Copy, Clone)] struct IndirectBatch { batch_id: u32, offset: u32, @@ -123,18 +123,19 @@ pub struct MeshRenderer { // TODO: move higher to deduplicate globally pub materials: HashMap>, - batches: Slab, + batches: Vec, draws: Vec, - draw_map: BTreeMap, + sorted_draws: Vec, + entity_locations: BTreeMap, batch_map: HashMap, indirect_draws: Vec, - indirect_batches: Vec, + indirect_batches: Vec>, mesh_buffer: MeshBuffer, shader_library: Arc, shader_factory: ShaderFactory, - updated_object_indexes: Query<(EntityIds, ChangeFilter), (All, With)>, - removed_rx: flume::Receiver<(Entity, RendererLocation)>, + updated_object_indexes: Query<(EntityIds, Component, ChangeFilter)>, + removed_rx: flume::Receiver<(Entity, usize)>, cull: ObjectCulling, new_object_query: Query, needs_indirect_rebuild: bool, @@ -188,15 +189,19 @@ impl MeshRenderer { shader_factory: Box::new(|v| v), removed_rx, draws: Vec::new(), - draw_map: Default::default(), - updated_object_indexes: Query::new((entity_ids(), object_buffer_index().modified())) - .with(renderer_location(id)), + updated_object_indexes: Query::new(( + entity_ids(), + renderer_location(id), + object_buffer_index().modified(), + )), new_object_query, indirect_draws: Vec::new(), indirect_batches: Vec::new(), object_buffer_gen: 0, skin_buffer_gen: 0, needs_indirect_rebuild: true, + entity_locations: BTreeMap::new(), + sorted_draws: Vec::new(), } } @@ -339,7 +344,10 @@ impl MeshRenderer { Entry::Occupied(slot) => *slot.get(), Entry::Vacant(slot) => { let batch = create_batch(slot.key()); - *slot.insert(self.batches.insert(batch?)) + let batch_index = self.batches.len(); + self.batches.push(batch?); + slot.insert(batch_index); + batch_index } }; @@ -350,9 +358,10 @@ impl MeshRenderer { id, }; - new_components.push((id, RendererLocation { batch_id })); + let new_index = self.draws.len(); + new_components.push((id, new_index)); + self.entity_locations.insert(id, new_index); - self.draw_map.insert(id, self.draws.len()); self.draws.push(draw); self.needs_indirect_rebuild = true; } @@ -368,9 +377,24 @@ impl MeshRenderer { let mut total_object_count = 0; self.indirect_draws.clear(); self.indirect_batches.clear(); - self.draws.sort_by_key(|v| v.batch_id); + self.indirect_batches.resize(self.batches.len(), None); + self.indirect_draws.resize( + self.batches.len(), + DrawIndexedIndirectArgs { + index_count: 0, + instance_count: 0, + first_index: 0, + base_vertex: 0, + first_instance: 0, + }, + ); - let chunks = self.draws.iter().chunk_by(|v| v.batch_id); + self.sorted_draws.clear(); + self.sorted_draws.extend(self.draws.iter().copied()); + // sort same batches by id, to ensure stable rendering + self.sorted_draws.sort_by_key(|v| (v.batch_id, v.id)); + + let chunks = self.sorted_draws.iter().chunk_by(|v| v.batch_id); for (batch_id, group) in &chunks { let instance_count = group.count() as u32; let batch = &self.batches[batch_id as usize]; @@ -384,35 +408,43 @@ impl MeshRenderer { let batch = IndirectBatch { batch_id, - offset: self.indirect_draws.len() as u32, + offset: batch_id, }; - self.indirect_draws.push(cmd); - self.indirect_batches.push(batch); + self.indirect_draws[batch_id as usize] = cmd; + self.indirect_batches[batch_id as usize] = Some(batch); total_object_count += instance_count; } - self.cull.update_objects(gpu, &self.draws); + self.cull.update_objects(gpu, &self.sorted_draws); } pub fn process_moved_objects(&mut self, world: &World) { - for (id, &index) in self.updated_object_indexes.borrow(world).iter() { - let draw_index = *self.draw_map.get(&id).unwrap(); - self.draws[draw_index].object_index = index as u32; + for (id, &loc, &new_index) in self.updated_object_indexes.borrow(world).iter() { + assert_eq!(self.draws[loc].id, id); + self.draws[loc].object_index = new_index as u32; + self.needs_indirect_rebuild = true } } - pub fn handle_removed(&mut self) { + pub fn process_removed(&mut self, world: &World) { for (id, _) in self.removed_rx.try_iter() { self.needs_indirect_rebuild = true; - let index = self.draw_map.remove(&id).expect("Object not in renderer"); - if index == self.draws.len() - 1 { + + let loc = self.entity_locations.remove(&id).unwrap(); + if loc == self.draws.len() - 1 { self.draws.pop(); } else { - self.draws.swap_remove(index); - let swapped = &self.draws[index]; - assert_eq!(self.draw_map.get(&swapped.id), Some(&self.draws.len())); - *self.draw_map.get_mut(&swapped.id).unwrap() = index; + let end = self.draws.len() - 1; + self.draws.swap_remove(loc); + + let swapped = &self.draws[loc]; + + self.entity_locations.insert(swapped.id, loc); + let _ = world.update(swapped.id, renderer_location(self.id), |v| { + assert_eq!(*v, end); + *v = loc; + }); } } } @@ -431,7 +463,7 @@ impl CameraRenderer for MeshRenderer { )?; self.process_moved_objects(ctx.world); - self.handle_removed(); + self.process_removed(ctx.world); if self.needs_indirect_rebuild { self.needs_indirect_rebuild = false; @@ -456,7 +488,6 @@ impl CameraRenderer for MeshRenderer { self.object_buffer_gen = object_buffer.gen(); self.skin_buffer_gen = skinning_buffer.gen(); - tracing::info!("discarding renderer bind group"); self.bind_group = None; self.cull.bind_group = None; } @@ -522,6 +553,10 @@ impl CameraRenderer for MeshRenderer { self.mesh_buffer.bind(render_pass); for draw in &self.indirect_batches { + let Some(draw) = draw else { + continue; + }; + let batch = &self.batches[draw.batch_id as usize]; if let Some(bind_group) = batch.material.bind_group() { @@ -542,11 +577,6 @@ impl CameraRenderer for MeshRenderer { type BatchId = usize; -#[derive(Debug, Clone, PartialEq, Eq, Copy)] -struct RendererLocation { - batch_id: BatchId, -} - flax::component! { - renderer_location(id): RendererLocation, + renderer_location(id): usize, } diff --git a/ivy-wgpu/src/renderer/mod.rs b/ivy-wgpu/src/renderer/mod.rs index f384bc8..6711ede 100644 --- a/ivy-wgpu/src/renderer/mod.rs +++ b/ivy-wgpu/src/renderer/mod.rs @@ -15,10 +15,10 @@ use ivy_assets::{ AssetCache, }; use ivy_core::{ - components::{main_camera, world_transform}, + components::{color, main_camera, world_transform}, impl_for_tuples, palette::Srgb, - to_linear_vec3, Bundle, + to_linear_vec3, Bundle, Color, ColorExt, }; use ivy_wgpu_types::shader::TargetDesc; pub use light_manager::LightManager; @@ -105,7 +105,7 @@ pub struct RenderContext<'a> { pub assets: &'a AssetCache, pub gpu: &'a Gpu, pub queue: &'a Queue, - pub store: &'a mut RendererStore, + pub store: &'a RendererStore, pub object_manager: &'a ObjectManager, // pub camera_data: &'a CameraShaderData, pub layouts: &'a [&'a BindGroupLayout], @@ -580,18 +580,23 @@ impl CameraShaderData { pub struct RenderObjectBundle<'a> { pub mesh: MeshDesc, + pub color: Color, pub materials: &'a [(Component, MaterialData)], } impl<'a> RenderObjectBundle<'a> { pub fn new(mesh: MeshDesc, materials: &'a [(Component, MaterialData)]) -> Self { - Self { mesh, materials } + Self { + mesh, + materials, + color: Color::white(), + } } } impl Bundle for RenderObjectBundle<'_> { fn mount(self, entity: &mut flax::EntityBuilder) { - entity.set(mesh(), self.mesh); + entity.set(mesh(), self.mesh).set(color(), self.color); for (pass, material) in self.materials { entity.set(*pass, material.clone()); diff --git a/ivy-wgpu/src/renderer/object_manager.rs b/ivy-wgpu/src/renderer/object_manager.rs index f487478..fd060e5 100644 --- a/ivy-wgpu/src/renderer/object_manager.rs +++ b/ivy-wgpu/src/renderer/object_manager.rs @@ -1,8 +1,10 @@ +use std::collections::BTreeMap; + use bytemuck::Zeroable; use flax::{ component, components::child_of, - fetch::{entity_refs, Copied, Modified, Source, TransformFetch, Traverse}, + fetch::{entity_refs, Modified, Source, TransformFetch, Traverse}, filter::{All, With}, Component, Entity, Fetch, FetchExt, Query, World, }; @@ -11,7 +13,7 @@ use ivy_assets::Asset; use ivy_core::{ components::{color, world_transform}, palette::WithAlpha, - profiling::profile_function, + profiling::{profile_function, profile_scope}, subscribers::RemovedComponentSubscriber, to_linear_vec3, Color, WorldExt, }; @@ -48,23 +50,20 @@ impl RenderObjectData { #[derive(Fetch)] #[fetch(transforms = [ Modified ])] struct ObjectDataQuery { - transform: Source>, Traverse>, - color: Source>, Traverse>, + transform: Component, + color: Component, } impl ObjectDataQuery { pub fn new() -> Self { Self { - transform: world_transform().copied().traverse(child_of), - color: color().copied().traverse(child_of), + transform: world_transform(), + color: color(), } } } -type UpdateFetch = ( - Component, - >::Output, -); +type UpdateFetch = (Component, Source); type SkinUpdateFetch = ( Component, @@ -80,6 +79,7 @@ type SkinUpdateFetch = ( pub struct ObjectManager { object_data: Vec, object_map: Vec, + entity_locations: BTreeMap, object_buffer: TypedBuffer, @@ -118,8 +118,11 @@ impl ObjectManager { object_map: Vec::new(), object_buffer, removed_rx, - object_query: Query::new((object_buffer_index(), ObjectDataQuery::new().modified())) - .with(mesh()), + object_query: Query::new(( + object_buffer_index(), + ObjectDataQuery::new().traverse(child_of), + )) + .with(mesh()), skin_query: Query::new(( object_buffer_index(), object_skinning_buffer(), @@ -128,6 +131,7 @@ impl ObjectManager { .with(mesh()), skinning_data: vec![Mat4::IDENTITY; skinning_buffer.len()], skinning_buffer, + entity_locations: BTreeMap::new(), } } @@ -154,7 +158,6 @@ impl ObjectManager { for (entity, (&transform, skin)) in &mut query.borrow(world) { let id = entity.id(); - let new_index = self.object_data.len(); let skin_buffer_offset = match skin { Some(skin) => { @@ -175,6 +178,7 @@ impl ObjectManager { None => None, }; + let new_index = self.object_data.len(); new_components.push((id, new_index)); self.object_data.push(RenderObjectData::new( @@ -184,6 +188,7 @@ impl ObjectManager { )); self.object_map.push(id); + self.entity_locations.insert(id, new_index); } world @@ -204,19 +209,23 @@ impl ObjectManager { pub fn process_removed(&mut self, world: &World) { profile_function!(); - for (id, loc) in self.removed_rx.try_iter() { + for (id, _) in self.removed_rx.try_iter() { + let loc = self.entity_locations.remove(&id).unwrap(); if loc == self.object_data.len() - 1 { - assert!(self.object_map[loc] == id); self.object_map.pop(); self.object_data.pop(); } else { let end = self.object_data.len() - 1; - self.object_data.swap(loc, end); - self.object_map.swap(loc, end); + self.object_data.swap_remove(loc); + self.object_map.swap_remove(loc); let swapped_entity = self.object_map[loc]; - let _ = world.update_dedup(swapped_entity, object_buffer_index(), loc); + self.entity_locations.insert(swapped_entity, loc); + let _ = world.update(swapped_entity, object_buffer_index(), |v| { + assert_eq!(*v, end); + *v = loc; + }); } } } @@ -226,11 +235,14 @@ impl ObjectManager { for (&loc, item) in &mut self.object_query.borrow(world) { assert_ne!(loc, usize::MAX); let object_data = &mut self.object_data[loc]; - object_data.transform = item.transform; + object_data.transform = *item.transform; object_data.color = to_linear_vec3(item.color.without_alpha()) } - self.object_buffer.write(&gpu.queue, 0, &self.object_data); + { + profile_scope!("upload_object_data"); + self.object_buffer.write(&gpu.queue, 0, &self.object_data); + } } fn update_skin_data(&mut self, world: &World, gpu: &Gpu) { @@ -274,11 +286,6 @@ impl ObjectManager { } } -// pub struct ObjectBufferLoc { -// index: usize, -// skin_buffer_offset: Option>, -// } - component! { pub(crate) object_buffer_index: usize, pub(crate) object_skinning_buffer: SubBuffer, diff --git a/ivy-wgpu/src/renderer/shadowmapping.rs b/ivy-wgpu/src/renderer/shadowmapping.rs index d69fa04..6d035e3 100644 --- a/ivy-wgpu/src/renderer/shadowmapping.rs +++ b/ivy-wgpu/src/renderer/shadowmapping.rs @@ -400,7 +400,7 @@ impl Node for ShadowMapNode { assets: ctx.assets, gpu: ctx.gpu, queue: ctx.queue, - store: &mut self.store, + store: &self.store, bind_groups: &[bind_group], layouts: &[&self.layout], target_desc: TargetDesc { diff --git a/ivy-wgpu/src/rendergraph/mod.rs b/ivy-wgpu/src/rendergraph/mod.rs index ae57d38..214fc2c 100644 --- a/ivy-wgpu/src/rendergraph/mod.rs +++ b/ivy-wgpu/src/rendergraph/mod.rs @@ -293,12 +293,10 @@ impl RenderGraph { profile_function!(); if self.order.is_none() { - tracing::info!("rebuilding"); self.build()?; } if mem::take(&mut self.resources.dirty) { - tracing::info!("dirty resources"); self.invoke_on_resource_modified(); self.allocate_resources(gpu)?;