tirbofish/dropbear · diff
fix: for some reason, the normals were all mixed up. fixed that up
fix: made mouse delta be clamped to the FPS
fix: got shaders back to working.
fix: proc objects use default textures instead of all white (incl emissive and diffuse).
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -1,3 +1,6 @@ [target.x86_64-unknown-linux-gnu] linker = "clang" rustflags = ["-C", "link-arg=-fuse-ld=mold", "-C", "relocation-model=pic"] + +# note: if you get a compile error such as "Recompile with RPIC", just run `cargo build` +# instead of `cargo build -p eucalyptus-core -p eucalyptus-editor`. @@ -1,9 +1,17 @@ +use crate::buffer::{ResizableBuffer, UniformBuffer}; use crate::graphics::SharedGraphicsContext; use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform}; use glam::Mat4; use std::collections::HashMap; use std::sync::Arc; -use wgpu::util::DeviceExt; + +#[repr(C)] +#[derive(Copy, Clone, Default, Debug, bytemuck::Pod, bytemuck::Zeroable)] +pub struct MorphTargetInfo { + num_vertices: u32, + num_targets: u32, + _padding: [u32; 2], +} #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AnimationComponent { @@ -27,7 +35,13 @@ pub struct AnimationComponent { pub skinning_matrices: Vec<Mat4>, #[serde(skip)] - pub bone_buffer: Option<wgpu::Buffer>, + pub skinning_buffer: Option<ResizableBuffer<Mat4>>, + #[serde(skip)] + pub morph_deltas_buffer: Option<ResizableBuffer<f32>>, + #[serde(skip)] + pub morph_weights_buffer: Option<ResizableBuffer<f32>>, + #[serde(skip)] + pub morph_info_buffer: Option<UniformBuffer<MorphTargetInfo>>, #[serde(skip)] pub available_animations: Vec<String>, @@ -37,9 +51,6 @@ pub struct AnimationComponent { #[serde(skip)] pub morph_weights: HashMap<usize, Vec<f32>>, - - #[serde(skip)] - pub morph_buffer: Option<wgpu::Buffer>, } #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] @@ -76,11 +87,13 @@ impl Default for AnimationComponent { animation_settings: HashMap::new(), local_pose: HashMap::new(), skinning_matrices: Vec::new(), - bone_buffer: None, available_animations: vec![], last_animation_index: None, morph_weights: HashMap::new(), - morph_buffer: None, + skinning_buffer: None, + morph_deltas_buffer: None, + morph_weights_buffer: None, + morph_info_buffer: None, } } } @@ -442,54 +455,76 @@ impl AnimationComponent { } pub fn prepare_gpu_resources(&mut self, graphics: Arc<SharedGraphicsContext>) { - if self.skinning_matrices.is_empty() { + let has_skinning = !self.skinning_matrices.is_empty(); + let has_morph_weights = !self.morph_weights.is_empty(); + + if !has_skinning && !has_morph_weights { return; } - if !self.morph_weights.is_empty() { + if has_skinning { + let buffer = self.skinning_buffer.get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + self.skinning_matrices.len(), + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + "skinning buffer", + ) + }); + + buffer.write(&graphics.device, &graphics.queue, &self.skinning_matrices); + } + + if has_skinning || has_morph_weights { let mut flat: Vec<f32> = Vec::new(); let mut sorted_nodes: Vec<usize> = self.morph_weights.keys().cloned().collect(); sorted_nodes.sort(); + let mut num_targets: usize = 0; + for node_idx in &sorted_nodes { - flat.extend_from_slice(&self.morph_weights[node_idx]); + let weights = &self.morph_weights[node_idx]; + num_targets = num_targets.max(weights.len()); + flat.extend_from_slice(weights); } - let data = bytemuck::cast_slice(&flat); - - if let Some(buffer) = &self.morph_buffer { - if buffer.size() as usize == data.len() { - graphics.queue.write_buffer(buffer, 0, data); - } else { - // size changed (different animation, different target count) — recreate - self.morph_buffer = None; - } + if flat.is_empty() { + flat.push(0.0); } - if self.morph_buffer.is_none() { - let buffer = graphics.device.create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("morph weights buffer"), - contents: data, - usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST, - }); + let weights_buffer = self.morph_weights_buffer.get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + flat.len().max(1), + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + "morph weights buffer", + ) + }); + + weights_buffer.write(&graphics.device, &graphics.queue, &flat); + + let info = MorphTargetInfo { + num_vertices: 0, + num_targets: num_targets as u32, + _padding: [0; 2], + }; - self.morph_buffer = Some(buffer); - } - } + let info_buffer = self.morph_info_buffer.get_or_insert_with(|| { + UniformBuffer::new(&graphics.device, "morph info buffer") + }); - let data = bytemuck::cast_slice(&self.skinning_matrices); + info_buffer.write(&graphics.queue, &info); - if let Some(buffer) = &self.bone_buffer { - graphics.queue.write_buffer(buffer, 0, data); - } else { - let buffer = graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some("skinning buffer"), - contents: data, - usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, - }); + let morph_deltas = [0.0f32]; + let deltas_buffer = self.morph_deltas_buffer.get_or_insert_with(|| { + ResizableBuffer::new( + &graphics.device, + morph_deltas.len(), + wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST, + "morph deltas buffer", + ) + }); - self.bone_buffer = Some(buffer); + deltas_buffer.write(&graphics.device, &graphics.queue, &morph_deltas); } } } @@ -2,34 +2,6 @@ use std::marker::PhantomData; -#[repr(C)] -#[derive(Copy, Clone, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)] -pub struct Vertex { - pub position: [f32; 3], - pub tex_coords: [f32; 2], -} - -impl Vertex { - pub fn desc() -> wgpu::VertexBufferLayout<'static> { - wgpu::VertexBufferLayout { - array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress, - step_mode: wgpu::VertexStepMode::Vertex, - attributes: &[ - wgpu::VertexAttribute { - offset: 0, - shader_location: 0, - format: wgpu::VertexFormat::Float32x3, - }, - wgpu::VertexAttribute { - offset: std::mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, - shader_location: 1, - format: wgpu::VertexFormat::Float32x2, - }, - ], - } - } -} - #[derive(Debug, Clone)] pub struct ResizableBuffer<T> { buffer: wgpu::Buffer, @@ -1,3 +1,4 @@ +use crate::model::Vertex; use crate::{BindGroupLayouts, texture}; use crate::{State, egui_renderer::EguiRenderer}; use dropbear_future_queue::FutureQueue; @@ -118,8 +119,8 @@ pub struct InstanceRaw { normal: [[f32; 3]; 3], } -impl InstanceRaw { - pub fn desc() -> VertexBufferLayout<'static> { +impl Vertex for InstanceRaw { + fn desc() -> VertexBufferLayout<'static> { VertexBufferLayout { array_stride: size_of::<InstanceRaw>() as BufferAddress, step_mode: wgpu::VertexStepMode::Instance, @@ -334,22 +334,19 @@ impl BindGroupLayouts { }); let light_cube_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("light cube bind group layout"), + label: Some("light cube light bind group layout"), entries: &[ - // u_camera + // u_light wgpu::BindGroupLayoutEntry { binding: 0, visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, count: None, }, - // u_light - wgpu::BindGroupLayoutEntry { - binding: 1, - visibility: wgpu::ShaderStages::VERTEX, - ty: wgpu::BindingType::Buffer { ty: wgpu::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: None }, - count: None, - } ], }); @@ -271,6 +271,7 @@ pub struct Light { pub cube_model: Handle<Model>, pub label: String, pub buffer: UniformBuffer<LightUniform>, + pub bind_group: wgpu::BindGroup, pub instance_buffer: ResizableBuffer<InstanceInput>, pub component: LightComponent, } @@ -308,6 +309,14 @@ impl Light { let label_str = label.unwrap_or("Light").to_string(); let buffer = UniformBuffer::new(&graphics.device, &label_str); + let bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some(&format!("{} light bind group", label_str)), + layout: &graphics.layouts.light_cube_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.buffer().as_entire_binding(), + }], + }); let transform = light.to_transform(); let instance: InstanceInput = DMat4::from_scale_rotation_translation( @@ -333,6 +342,7 @@ impl Light { cube_model, label: label_str, buffer, + bind_group, instance_buffer, component: light.clone(), } @@ -1043,11 +1043,21 @@ impl Model { [255, 255, 255, 255], Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), ); + let black_srgb_texture = registry.solid_texture_rgba8_with_format( + graphics.clone(), + [0, 0, 0, 255], + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), + ); let white_linear_texture = registry.solid_texture_rgba8_with_format( graphics.clone(), [255, 255, 255, 255], Texture::TEXTURE_FORMAT_BASE, ); + let green_linear_texture = registry.solid_texture_rgba8_with_format( + graphics.clone(), + [0, 255, 0, 255], + Texture::TEXTURE_FORMAT_BASE, + ); let flat_normal_texture = registry.solid_texture_rgba8_with_format( graphics.clone(), [128, 128, 255, 255], @@ -1140,7 +1150,7 @@ impl Model { let emissive_texture_bound = emissive_texture .clone() - .or_else(|| registry.get_texture(white_srgb_texture).cloned()) + .or_else(|| registry.get_texture(black_srgb_texture).cloned()) .ok_or_else(|| { anyhow::anyhow!( "Unable to resolve emissive fallback texture for model {:?}", @@ -1149,7 +1159,7 @@ impl Model { })?; let metallic_roughness_texture_bound = metallic_roughness_texture .clone() - .or_else(|| registry.get_texture(white_linear_texture).cloned()) + .or_else(|| registry.get_texture(green_linear_texture).cloned()) .ok_or_else(|| { anyhow::anyhow!( "Unable to resolve metallic fallback texture for model {:?}", @@ -17,8 +17,6 @@ pub struct LightCubePipeline { pipeline_layout: wgpu::PipelineLayout, pipeline: wgpu::RenderPipeline, storage_buffer: Option<StorageBuffer<LightArrayUniform>>, - /// Bind group, defined in `shaders/shader.wgsl` as @group(2) - light_bind_group: wgpu::BindGroup, } impl DropbearShaderPipeline for LightCubePipeline { @@ -34,6 +32,7 @@ impl DropbearShaderPipeline for LightCubePipeline { .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("light cube pipeline layout"), bind_group_layouts: &[ + &graphics.layouts.camera_bind_group_layout, &graphics.layouts.light_cube_layout, ], push_constant_ranges: &[], @@ -98,25 +97,11 @@ impl DropbearShaderPipeline for LightCubePipeline { let storage_buffer = StorageBuffer::new(&graphics.device, "light cube pipeline storage buffer"); - let light_buffer: &wgpu::Buffer = storage_buffer.buffer(); - - let light_bind_group = graphics - .device - .create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.layouts.light_cube_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: light_buffer.as_entire_binding(), - }], - label: Some("light array bind group"), - }); - Self { shader, pipeline_layout, pipeline, storage_buffer: Some(storage_buffer), - light_bind_group, } } @@ -134,10 +119,6 @@ impl DropbearShaderPipeline for LightCubePipeline { } impl LightCubePipeline { - pub fn bind_group(&self) -> &wgpu::BindGroup { - &self.light_bind_group - } - pub fn light_buffer(&self) -> &wgpu::Buffer { self.storage_buffer .as_ref() @@ -82,43 +82,63 @@ impl ProcedurallyGeneratedObject { }; let material = material.unwrap_or_else(|| { - let flat_normal_handle = _rguard.solid_texture_rgba8_with_format( + let white_srgb_texture = _rguard.solid_texture_rgba8_with_format( graphics.clone(), - [128, 128, 255, 255], - Texture::TEXTURE_FORMAT_BASE, + [255, 255, 255, 255], + Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), ); - let white_srgb_handle = _rguard.solid_texture_rgba8_with_format( + let black_srgb_texture = _rguard.solid_texture_rgba8_with_format( graphics.clone(), - [255, 255, 255, 255], + [0, 0, 0, 255], Texture::TEXTURE_FORMAT_BASE.add_srgb_suffix(), ); - let white_linear_handle = _rguard.solid_texture_rgba8_with_format( + let white_linear_texture = _rguard.solid_texture_rgba8_with_format( graphics.clone(), [255, 255, 255, 255], Texture::TEXTURE_FORMAT_BASE, ); - let flat_normal = _rguard - .get_texture(flat_normal_handle) - .expect("Flat normal texture handle missing") - .clone(); + let green_linear_texture = _rguard.solid_texture_rgba8_with_format( + graphics.clone(), + [0, 255, 0, 255], + Texture::TEXTURE_FORMAT_BASE, + ); + let flat_normal_texture = _rguard.solid_texture_rgba8_with_format( + graphics.clone(), + [128, 128, 255, 255], + Texture::TEXTURE_FORMAT_BASE, + ); + let white_srgb = _rguard - .get_texture(white_srgb_handle) - .expect("White SRGB texture handle missing") - .clone(); + .get_texture(white_srgb_texture) + .cloned() + .expect("Missing procedural white srgb texture"); + let black_srgb = _rguard + .get_texture(black_srgb_texture) + .cloned() + .expect("Missing procedural black srgb texture"); let white_linear = _rguard - .get_texture(white_linear_handle) - .expect("White linear texture handle missing") - .clone(); + .get_texture(white_linear_texture) + .cloned() + .expect("Missing procedural white linear texture"); + let green_linear = _rguard + .get_texture(green_linear_texture) + .cloned() + .expect("Missing procedural green linear texture"); + let flat_normal = _rguard + .get_texture(flat_normal_texture) + .cloned() + .expect("Missing procedural flat normal texture"); + Material::new( graphics.clone(), "procedural_material", - white_srgb.clone(), + white_srgb, flat_normal, None, None, None, - white_srgb, - white_linear.clone(), + black_srgb, + green_linear, white_linear, false, [1.0, 1.0, 1.0, 1.0], @@ -3,7 +3,7 @@ [[vk::binding(0, 0)]] ConstantBuffer<CameraUniform> u_camera; -[[vk::binding(1, 0)]] +[[vk::binding(0, 1)]] ConstantBuffer<Light> u_light; struct VertexInput { @@ -326,8 +326,14 @@ impl Mouse for Editor { .query_one::<(&mut Camera, &CameraComponent)>(active_camera) .get() { + let frame_scale = if self.dt > 0.0 { + (self.dt as f64 / (1.0 / 60.0)).clamp(0.1, 4.0) + } else { + 1.0 + }; + if let Some((dx, dy)) = delta { - camera.track_mouse_delta(dx, dy); + camera.track_mouse_delta(dx * frame_scale, dy * frame_scale); self.input_state.mouse_delta = Some((dx, dy)); } else { log_once::warn_once!("Unable to track mouse delta, attempting fallback"); @@ -335,7 +341,7 @@ impl Mouse for Editor { if let Some(old_mouse_pos) = self.input_state.last_mouse_pos { let dx = position.x - old_mouse_pos.0; let dy = position.y - old_mouse_pos.1; - camera.track_mouse_delta(dx, dy); + camera.track_mouse_delta(dx * frame_scale, dy * frame_scale); self.input_state.mouse_delta = Some((dx, dy)); log_once::debug_once!("Fallback mouse tracking used"); } else { @@ -77,6 +77,7 @@ use winit::window::{CursorGrabMode, WindowAttributes}; use winit::{keyboard::KeyCode, window::Window}; pub struct Editor { + pub dt: f32, pub scene_command: SceneCommand, pub world: Box<World>, pub physics_state: PhysicsState, @@ -314,6 +315,7 @@ impl Editor { default_morph_weights_buffer: None, default_morph_info_buffer: None, default_animation_bind_group: None, + dt: 60.0, }) } @@ -17,7 +17,6 @@ use eucalyptus_core::physics::collider::ColliderShapeKey; use eucalyptus_core::physics::collider::shader::{ColliderInstanceRaw, create_wireframe_geometry}; use eucalyptus_core::properties::CustomProperties; use eucalyptus_core::states::{Label, WorldLoadingStatus}; -use glam::{Mat4}; use log; use parking_lot::Mutex; use std::collections::HashMap; @@ -100,6 +99,8 @@ impl Scene for Editor { dt: f32, graphics: std::sync::Arc<dropbear_engine::graphics::SharedGraphicsContext>, ) { + self.dt = dt; + if let Some(rx) = &self.play_mode_exit_rx { if rx.try_recv().is_ok() { log::info!("Play mode process has exited, returning to editing mode"); @@ -347,12 +348,6 @@ impl Scene for Editor { log_once::debug_once!("Camera ready"); log_once::debug_once!("Camera currently being viewed: {}", camera.label); - let Some(pipeline) = &self.main_render_pipeline else { - log_once::warn_once!("Render pipeline not ready"); - return; - }; - log_once::debug_once!("Pipeline ready"); - let Some(camera_bind_group) = self.camera_bind_group.as_ref() else { log_once::warn_once!("Camera bind group not ready"); return; @@ -414,14 +409,20 @@ impl Scene for Editor { } let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new(); - let mut animated_instances: Vec<(u64, InstanceRaw, Vec<Mat4>, Vec<f32>, u32, u32)> = Vec::new(); + let mut animated_instances: Vec<( + u64, + InstanceRaw, + wgpu::Buffer, + wgpu::Buffer, + wgpu::Buffer, + wgpu::Buffer, + )> = Vec::new(); { - puffin::profile_scope!("Locating all renderers and animation components"); - let registry = ASSET_REGISTRY.read(); + puffin::profile_scope!("finding all renderers and animation components"); let mut query = self .world - .query::<(&MeshRenderer, Option<&AnimationComponent>)>(); + .query::<(&MeshRenderer, Option<&mut AnimationComponent>)>(); for (renderer, animation) in query.iter() { let handle = renderer.model(); @@ -431,50 +432,54 @@ impl Scene for Editor { let instance = renderer.instance.to_raw(); if let Some(animation) = animation { - if !animation.skinning_matrices.is_empty() { - let morph_weights = if animation.morph_weights.is_empty() { - Vec::new() - } else { - let mut sorted_nodes: Vec<usize> = - animation.morph_weights.keys().cloned().collect(); - sorted_nodes.sort(); - let mut flat = Vec::new(); - for node_idx in &sorted_nodes { - if let Some(weights) = animation.morph_weights.get(node_idx) { - flat.extend_from_slice(weights); - } - } - flat - }; - - let num_targets = animation - .morph_weights - .values() - .next() - .map(|weights| weights.len() as u32) - .unwrap_or(0); - let num_vertices = registry - .get_model(handle) - .map(|model| { - model - .meshes - .iter() - .map(|mesh| mesh.vertices.len()) - .sum::<usize>() as u32 - }) - .unwrap_or(0); - - animated_instances.push(( - handle.id, - instance, - animation.skinning_matrices.clone(), - morph_weights, - num_vertices, - num_targets, - )); - } else { + if animation.skinning_matrices.is_empty() { static_batches.entry(handle.id).or_default().push(instance); + continue; } + + animation.prepare_gpu_resources(graphics.clone()); + + let Some(skinning_buffer) = animation + .skinning_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + let Some(morph_deltas_buffer) = animation + .morph_deltas_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + let Some(morph_weights_buffer) = animation + .morph_weights_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + let Some(morph_info_buffer) = animation + .morph_info_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + + animated_instances.push(( + handle.id, + instance, + skinning_buffer, + morph_deltas_buffer, + morph_weights_buffer, + morph_info_buffer, + )); } else { static_batches.entry(handle.id).or_default().push(instance); } @@ -543,54 +548,38 @@ impl Scene for Editor { continue; }; - render_pass.draw_light_model(model, camera_bind_group, light_pipeline.bind_group()); + render_pass.draw_light_model(model, camera_bind_group, &light.bind_group); } } } - let default_skinning_buffer = self - .default_skinning_buffer - .as_ref() - .expect("Default skinning buffer not initialized"); - let default_animation_bind_group = self - .default_animation_bind_group - .as_ref() - .expect("Default animation bind group not initialized"); - let default_morph_weights_buffer = self - .default_morph_weights_buffer - .as_ref() - .expect("Default morph weights buffer not initialized"); - let default_morph_info_buffer = self - .default_morph_info_buffer - .as_ref() - .expect("Default morph info buffer not initialized"); - - graphics.queue.write_buffer( - default_skinning_buffer, - 0, - bytemuck::cast_slice(&[Mat4::IDENTITY]), - ); - graphics.queue.write_buffer( - default_morph_info_buffer, - 0, - bytemuck::cast_slice(&[0u32; 4]), - ); - // model rendering let sky = self .sky_pipeline .as_ref() - .expect("Sky pipeline must be initialized before rendering models"); + .expect("Sky pipeline must be initialised before rendering models"); let environment_bind_group = &sky.environment_bind_group; - let per_frame_bind_group = pipeline - .per_frame - .as_ref() - .expect("Per-frame bind group not initialized"); - + let Some(pipeline) = self.main_render_pipeline.as_mut() else { + log_once::warn_once!("Render pipeline not ready"); + return; + }; + log_once::debug_once!("Pipeline ready"); + + // static models if let Some(_) = &self.light_cube_pipeline { puffin::profile_scope!("model render pass"); for (model, handle, instance_count) in prepared_models { + let default_animation_bind_group = self + .default_animation_bind_group + .as_ref() + .expect("Default animation bind group not initialised"); + let per_frame_bind_group = pipeline + .per_frame + .as_ref() + .expect("Per-frame bind group not initialised") + .clone(); + let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("model render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -623,27 +612,51 @@ impl Scene for Editor { render_pass.draw_model_instanced( model, 0..instance_count, - per_frame_bind_group, + &per_frame_bind_group, default_animation_bind_group, environment_bind_group, ); } } - if let Some(_lcp) = &self.light_cube_pipeline { + // animated models + if let Some(_) = &self.light_cube_pipeline { puffin::profile_scope!("animated model render pass"); - for (handle, instance, skinning_matrices, morph_weights, num_vertices, num_targets) in animated_instances { + for ( + handle, + instance, + skinning_buffer, + morph_deltas_buffer, + morph_weights_buffer, + morph_info_buffer, + ) in animated_instances + { let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; }; + let animation_bind_group = pipeline + .animation_bind_group( + graphics.clone(), + &skinning_buffer, + &morph_deltas_buffer, + &morph_weights_buffer, + &morph_info_buffer, + ) + .clone(); + let per_frame_bind_group = pipeline + .per_frame + .as_ref() + .expect("Per-frame bind group not initialised") + .clone(); + let instance_buffer = self.animated_instance_buffer.get_or_insert_with(|| { ResizableBuffer::new( &graphics.device, 1, wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST, - "Runtime Animated Instance Buffer", + "animated instance buffer", ) }); instance_buffer.write(&graphics.device, &graphics.queue, &[instance]); @@ -674,41 +687,11 @@ impl Scene for Editor { render_pass.set_pipeline(pipeline.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); - let max_skinning_matrices = 256usize; - let matrices = if skinning_matrices.len() > max_skinning_matrices { - &skinning_matrices[..max_skinning_matrices] - } else { - &skinning_matrices[..] - }; - graphics.queue.write_buffer( - default_skinning_buffer, - 0, - bytemuck::cast_slice(matrices), - ); - - let weights = if morph_weights.len() > MAX_MORPH_WEIGHTS { - &morph_weights[..MAX_MORPH_WEIGHTS] - } else { - &morph_weights[..] - }; - if !weights.is_empty() { - graphics.queue.write_buffer( - default_morph_weights_buffer, - 0, - bytemuck::cast_slice(weights), - ); - } - graphics.queue.write_buffer( - default_morph_info_buffer, - 0, - bytemuck::cast_slice(&[num_vertices, num_targets, 0, 0]), - ); - render_pass.draw_model_instanced( model, 0..1, - per_frame_bind_group, - default_animation_bind_group, + &per_frame_bind_group, + &animation_bind_group, environment_bind_group, ); } @@ -1,7 +1,6 @@ use std::sync::Arc; use crate::PlayMode; -use crate::MAX_MORPH_WEIGHTS; use dropbear_engine::animation::AnimationComponent; use dropbear_engine::asset::{ASSET_REGISTRY, Handle}; use dropbear_engine::buffer::ResizableBuffer; @@ -26,7 +25,7 @@ use eucalyptus_core::rapier3d::prelude::QueryFilter; use eucalyptus_core::scene::loading::{IsSceneLoaded, SCENE_LOADER, SceneLoadResult}; use eucalyptus_core::states::SCENES; use eucalyptus_core::states::{Label, PROJECT}; -use glam::{DVec3, Mat4, Quat, Vec2}; +use glam::{DVec3, Quat, Vec2}; use hecs::Entity; // use kino_ui::widgets::rect::Rectangle; // use kino_ui::widgets::{Border, Fill}; @@ -650,12 +649,6 @@ impl Scene for PlayMode { log_once::debug_once!("Camera ready"); log_once::debug_once!("Camera currently being viewed: {}", camera.label); - let Some(pipeline) = &self.main_pipeline else { - log_once::warn_once!("Render pipeline not ready"); - return; - }; - log_once::debug_once!("Pipeline ready"); - let Some(camera_bind_group) = self.camera_bind_group.as_ref() else { log_once::warn_once!("Camera bind group not ready"); return; @@ -714,13 +707,19 @@ impl Scene for PlayMode { } let mut static_batches: HashMap<u64, Vec<InstanceRaw>> = HashMap::new(); - let mut animated_instances: Vec<(u64, InstanceRaw, Vec<Mat4>, Vec<f32>, u32, u32)> = Vec::new(); + let mut animated_instances: Vec<( + u64, + InstanceRaw, + wgpu::Buffer, + wgpu::Buffer, + wgpu::Buffer, + wgpu::Buffer, + )> = Vec::new(); { - let registry = ASSET_REGISTRY.read(); let mut query = self .world - .query::<(&MeshRenderer, Option<&AnimationComponent>)>(); + .query::<(&MeshRenderer, Option<&mut AnimationComponent>)>(); for (renderer, animation) in query.iter() { let handle = renderer.model(); @@ -730,50 +729,54 @@ impl Scene for PlayMode { let instance = renderer.instance.to_raw(); if let Some(animation) = animation { - if !animation.skinning_matrices.is_empty() { - let morph_weights = if animation.morph_weights.is_empty() { - Vec::new() - } else { - let mut sorted_nodes: Vec<usize> = - animation.morph_weights.keys().cloned().collect(); - sorted_nodes.sort(); - let mut flat = Vec::new(); - for node_idx in &sorted_nodes { - if let Some(weights) = animation.morph_weights.get(node_idx) { - flat.extend_from_slice(weights); - } - } - flat - }; - - let num_targets = animation - .morph_weights - .values() - .next() - .map(|weights| weights.len() as u32) - .unwrap_or(0); - let num_vertices = registry - .get_model(handle) - .map(|model| { - model - .meshes - .iter() - .map(|mesh| mesh.vertices.len()) - .sum::<usize>() as u32 - }) - .unwrap_or(0); - - animated_instances.push(( - handle.id, - instance, - animation.skinning_matrices.clone(), - morph_weights, - num_vertices, - num_targets, - )); - } else { + if animation.skinning_matrices.is_empty() { static_batches.entry(handle.id).or_default().push(instance); + continue; } + + animation.prepare_gpu_resources(graphics.clone()); + + let Some(skinning_buffer) = animation + .skinning_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + let Some(morph_deltas_buffer) = animation + .morph_deltas_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + let Some(morph_weights_buffer) = animation + .morph_weights_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + let Some(morph_info_buffer) = animation + .morph_info_buffer + .as_ref() + .map(|buffer| buffer.buffer().clone()) + else { + static_batches.entry(handle.id).or_default().push(instance); + continue; + }; + + animated_instances.push(( + handle.id, + instance, + skinning_buffer, + morph_deltas_buffer, + morph_weights_buffer, + morph_info_buffer, + )); } else { static_batches.entry(handle.id).or_default().push(instance); } @@ -841,52 +844,35 @@ impl Scene for PlayMode { continue; }; - render_pass.draw_light_model(model, camera_bind_group, light_pipeline.bind_group()); + render_pass.draw_light_model(model, camera_bind_group, &light.bind_group); } } } - let default_skinning_buffer = self - .default_skinning_buffer - .as_ref() - .expect("Default skinning buffer not initialised"); let default_animation_bind_group = self .default_animation_bind_group .as_ref() - .expect("Default animation bind group not initialized"); - let default_morph_weights_buffer = self - .default_morph_weights_buffer - .as_ref() - .expect("Default morph weights buffer not initialised"); - let default_morph_info_buffer = self - .default_morph_info_buffer - .as_ref() - .expect("Default morph info buffer not initialised"); - - graphics.queue.write_buffer( - default_skinning_buffer, - 0, - bytemuck::cast_slice(&[Mat4::IDENTITY]), - ); - graphics.queue.write_buffer( - default_morph_info_buffer, - 0, - bytemuck::cast_slice(&[0u32; 4]), - ); + .expect("Default animation bind group not initialised"); // model rendering let sky = self .sky_pipeline .as_ref() - .expect("Sky pipeline must be initialized before rendering models"); + .expect("Sky pipeline must be initialised before rendering models"); let environment_bind_group = &sky.environment_bind_group; - let per_frame_bind_group = pipeline - .per_frame - .as_ref() - .expect("Per-frame bind group not initialized"); + let Some(pipeline) = self.main_pipeline.as_mut() else { + log_once::warn_once!("Render pipeline not ready"); + return; + }; + log_once::debug_once!("Pipeline ready"); for (model, handle, instance_count) in prepared_models { + let per_frame_bind_group = pipeline + .per_frame + .as_ref() + .expect("Per-frame bind group not initialised") + .clone(); let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("model render pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { @@ -919,19 +905,42 @@ impl Scene for PlayMode { render_pass.draw_model_instanced( model, 0..instance_count, - per_frame_bind_group, + &per_frame_bind_group, default_animation_bind_group, environment_bind_group, ); } if let Some(_lcp) = &self.light_cube_pipeline { - for (handle, instance, skinning_matrices, morph_weights, num_vertices, num_targets) in animated_instances { + for ( + handle, + instance, + skinning_buffer, + morph_deltas_buffer, + morph_weights_buffer, + morph_info_buffer, + ) in animated_instances + { let Some(model) = registry.get_model(Handle::new(handle)) else { log_once::error_once!("Missing model handle {} in registry", handle); continue; }; + let animation_bind_group = pipeline + .animation_bind_group( + graphics.clone(), + &skinning_buffer, + &morph_deltas_buffer, + &morph_weights_buffer, + &morph_info_buffer, + ) + .clone(); + let per_frame_bind_group = pipeline + .per_frame + .as_ref() + .expect("Per-frame bind group not initialised") + .clone(); + let instance_buffer = self.animated_instance_buffer.get_or_insert_with(|| { ResizableBuffer::new( &graphics.device, @@ -968,41 +977,11 @@ impl Scene for PlayMode { render_pass.set_pipeline(pipeline.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(1)); - let max_skinning_matrices = 256usize; - let matrices = if skinning_matrices.len() > max_skinning_matrices { - &skinning_matrices[..max_skinning_matrices] - } else { - &skinning_matrices[..] - }; - graphics.queue.write_buffer( - default_skinning_buffer, - 0, - bytemuck::cast_slice(matrices), - ); - - let weights = if morph_weights.len() > MAX_MORPH_WEIGHTS { - &morph_weights[..MAX_MORPH_WEIGHTS] - } else { - &morph_weights[..] - }; - if !weights.is_empty() { - graphics.queue.write_buffer( - default_morph_weights_buffer, - 0, - bytemuck::cast_slice(weights), - ); - } - graphics.queue.write_buffer( - default_morph_info_buffer, - 0, - bytemuck::cast_slice(&[num_vertices, num_targets, 0, 0]), - ); - render_pass.draw_model_instanced( model, 0..1, - per_frame_bind_group, - default_animation_bind_group, + &per_frame_bind_group, + &animation_bind_group, environment_bind_group, ); }