tirbofish/dropbear · commit
d915a7291b7ae09c148ebe95696e01dab655574f
wip: animation
wip: animation scripting
feature: implemented puffin profiler into dropbear_engine
refactor: completely remade the asset server, with support for typesafe handles instead of using arbitrary values (fixes #87)
refactor: model importing to be able to export in a custom format during production
builds are still not available yet because the other crates are still fucked.
Signature present but could not be verified.
Unverified
@@ -31,7 +31,7 @@ env_logger = "0.11" futures = "0.3" gilrs = "0.11" git2 = { version = "0.20", features = ["vendored-openssl"] } -glam = { version = "0.30", features = ["serde", "mint"] } +glam = { version = "0.30", features = ["serde", "mint", "bytemuck"] } hecs = { version = "0.11", features = ["serde"] } log = "0.4" log-once = "0.4" @@ -84,6 +84,7 @@ glyphon = { git = "https://github.com/grovesNL/glyphon", rev = "9dd9376" } puffin = "0.19" bitflags = "2.10" features = "0.10" +puffin_http = "0.16" [workspace.dependencies.image] version = "0.24" @@ -47,6 +47,7 @@ pollster.workspace = true image.workspace = true puffin.workspace = true bitflags.workspace = true +puffin_http.workspace = true #yakui-wgpu.workspace = true #yakui.workspace = true @@ -0,0 +1,323 @@ +use dropbear_traits::SerializableComponent; +use std::collections::HashMap; +use std::sync::Arc; +use glam::Mat4; +use wgpu::util::DeviceExt; +use dropbear_macro::SerializableComponent; +use crate::graphics::SharedGraphicsContext; +use crate::model::{AnimationInterpolation, ChannelValues, Model, NodeTransform}; + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, SerializableComponent)] +pub struct AnimationComponent { + #[serde(default)] + pub active_animation_index: Option<usize>, + #[serde(default)] + pub time: f32, + #[serde(default)] + pub speed: f32, + #[serde(default)] + pub looping: bool, + #[serde(default)] + pub is_playing: bool, + + #[serde(skip)] + pub local_pose: HashMap<usize, NodeTransform>, + #[serde(skip)] + pub skinning_matrices: Vec<Mat4>, + + #[serde(skip)] + pub bone_buffer: Option<wgpu::Buffer>, + #[serde(skip)] + pub bind_group: Option<wgpu::BindGroup>, +} + +impl Default for AnimationComponent { + fn default() -> Self { + Self { + active_animation_index: None, + time: 0.0, + speed: 1.0, + looping: true, + is_playing: true, + local_pose: HashMap::new(), + skinning_matrices: Vec::new(), + bone_buffer: None, + bind_group: None, + } + } +} + +impl AnimationComponent { + pub fn new() -> Self { + Self::default() + } + + pub fn update(&mut self, dt: f32, model: &Model) { + puffin::profile_function!(&model.label); + if !self.is_playing || self.active_animation_index.is_none() { + return; + } + + let anim_idx = self.active_animation_index.unwrap(); + let animation = &model.animations[anim_idx]; + + self.time += dt * self.speed; + if self.looping { + if animation.duration > 0.0 { + self.time %= animation.duration; + } + } else { + self.time = self.time.clamp(0.0, animation.duration); + if self.time >= animation.duration { + self.is_playing = false; + } + } + + for channel in &animation.channels { + let count = channel.times.len(); + if count == 0 { continue; } + + // Handle out of bounds / single keyframe + if count == 1 || self.time <= channel.times[0] { + Self::apply_single_keyframe(channel, 0, &mut self.local_pose, model); + continue; + } + if self.time >= channel.times[count - 1] { + Self::apply_single_keyframe(channel, count - 1, &mut self.local_pose, model); + continue; + } + + let next_idx = channel.times.partition_point(|&t| t <= self.time); + let prev_idx = next_idx.saturating_sub(1); + + let start_time = channel.times[prev_idx]; + let end_time = channel.times[next_idx]; + let duration = end_time - start_time; + + let factor = if duration > 0.0 { + (self.time - start_time) / duration + } else { + 0.0 + }; + + let transform = self.local_pose + .entry(channel.target_node) + .or_insert_with(|| { + model.nodes.get(channel.target_node) + .map(|n| n.transform.clone()) + .unwrap_or_else(NodeTransform::identity) + }); + + let dt = end_time - start_time; + + match &channel.values { + ChannelValues::Translations(values) => { + transform.translation = match channel.interpolation { + AnimationInterpolation::Step => values[prev_idx], + AnimationInterpolation::Linear => { + let start = values[prev_idx]; + let end = values[next_idx]; + start.lerp(end, factor) + }, + AnimationInterpolation::CubicSpline => { + let t = factor; + let t2 = t * t; + let t3 = t2 * t; + + let idx0 = prev_idx * 3; + let idx1 = next_idx * 3; + + if idx1 + 1 >= values.len() { + values[idx0 + 1] + } else { + let p0 = values[idx0 + 1]; + let m0 = values[idx0 + 2] * dt; + let m1 = values[idx1 + 0] * dt; + let p1 = values[idx1 + 1]; + + let h00 = 2.0 * t3 - 3.0 * t2 + 1.0; + let h10 = t3 - 2.0 * t2 + t; + let h01 = -2.0 * t3 + 3.0 * t2; + let h11 = t3 - t2; + + p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11 + } + } + }; + } + ChannelValues::Rotations(values) => { + transform.rotation = match channel.interpolation { + AnimationInterpolation::Step => values[prev_idx], + AnimationInterpolation::Linear => { + let start = values[prev_idx]; + let end = values[next_idx]; + start.slerp(end, factor).normalize() + }, + AnimationInterpolation::CubicSpline => { + let t = factor; + let t2 = t * t; + let t3 = t2 * t; + + let idx0 = prev_idx * 3; + let idx1 = next_idx * 3; + + if idx1 + 1 >= values.len() { + values[idx0 + 1] + } else { + let p0 = values[idx0 + 1]; + let m0 = values[idx0 + 2] * dt; + let m1 = values[idx1 + 0] * dt; + let p1 = values[idx1 + 1]; + + let h00 = 2.0 * t3 - 3.0 * t2 + 1.0; + let h10 = t3 - 2.0 * t2 + t; + let h01 = -2.0 * t3 + 3.0 * t2; + let h11 = t3 - t2; + + let res = p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11; + res.normalize() + } + } + }; + } + ChannelValues::Scales(values) => { + transform.scale = match channel.interpolation { + AnimationInterpolation::Step => values[prev_idx], + AnimationInterpolation::Linear => { + let start = values[prev_idx]; + let end = values[next_idx]; + start.lerp(end, factor) + }, + AnimationInterpolation::CubicSpline => { + let t = factor; + let t2 = t * t; + let t3 = t2 * t; + + let idx0 = prev_idx * 3; + let idx1 = next_idx * 3; + + if idx1 + 1 >= values.len() { + values[idx0 + 1] + } else { + let p0 = values[idx0 + 1]; + let m0 = values[idx0 + 2] * dt; + let m1 = values[idx1 + 0] * dt; + let p1 = values[idx1 + 1]; + + let h00 = 2.0 * t3 - 3.0 * t2 + 1.0; + let h10 = t3 - 2.0 * t2 + t; + let h01 = -2.0 * t3 + 3.0 * t2; + let h11 = t3 - t2; + + p0 * h00 + m0 * h10 + p1 * h01 + m1 * h11 + } + } + }; + } + } + } + + self.update_matrices(model); + } + + fn apply_single_keyframe( + channel: &crate::model::AnimationChannel, + index: usize, + pose: &mut HashMap<usize, NodeTransform>, + model: &Model + ) { + let transform = pose.entry(channel.target_node).or_insert_with(|| { + model.nodes.get(channel.target_node) + .map(|n| n.transform.clone()) + .unwrap_or_else(NodeTransform::identity) + }); + + match &channel.values { + ChannelValues::Translations(v) => { + if let Some(val) = v.get(index) { transform.translation = *val; } + } + ChannelValues::Rotations(v) => { + if let Some(val) = v.get(index) { transform.rotation = *val; } + } + ChannelValues::Scales(v) => { + if let Some(val) = v.get(index) { transform.scale = *val; } + } + } + } + + fn update_matrices(&mut self, model: &Model) { + if let Some(skin) = model.skins.first() { + if self.skinning_matrices.len() != skin.joints.len() { + self.skinning_matrices.resize(skin.joints.len(), Mat4::IDENTITY); + } + + let mut global_transforms = HashMap::new(); + + for &joint_idx in &skin.joints { + self.resolve_global_transform(joint_idx, model, &mut global_transforms); + } + + for (i, &joint_node_idx) in skin.joints.iter().enumerate() { + if let Some(global_transform) = global_transforms.get(&joint_node_idx) { + let inverse_bind = skin.inverse_bind_matrices[i]; + self.skinning_matrices[i] = *global_transform * inverse_bind; + } + } + } + } + + /// Recursively calculates and caches the global world matrix for a node. + fn resolve_global_transform( + &self, + node_idx: usize, + model: &Model, + cache: &mut HashMap<usize, Mat4>, + ) -> Mat4 { + if let Some(&matrix) = cache.get(&node_idx) { + return matrix; + } + + let node = &model.nodes[node_idx]; + let local_matrix = self.local_pose.get(&node_idx) + .map(|transform| transform.to_matrix()) + .unwrap_or_else(|| node.transform.to_matrix()); + + let global_matrix = if let Some(parent_idx) = node.parent { + let parent_global = self.resolve_global_transform(parent_idx, model, cache); + parent_global * local_matrix + } else { + local_matrix + }; + + cache.insert(node_idx, global_matrix); + global_matrix + } + + pub fn prepare_gpu_resources(&mut self, graphics: Arc<SharedGraphicsContext>) { + if self.skinning_matrices.is_empty() { return; } + + let data = bytemuck::cast_slice(&self.skinning_matrices); + + 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 bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some("skinning bind group"), + layout: &graphics.layouts.skinning_bind_group_layout, + entries: &[wgpu::BindGroupEntry { + binding: 0, + resource: buffer.as_entire_binding(), + }], + }); + + self.bone_buffer = Some(buffer); + self.bind_group = Some(bind_group); + } + } +} @@ -119,6 +119,13 @@ impl AssetRegistry { self.texture_labels.insert(label.into(), handle.clone()); } + /// Removes a label from the texture registry, but keeps it in the registry. + /// + /// When the label is removed, the [Handle] is still valid. + pub fn remove_label_texture(&mut self, label: &str) { + self.texture_labels.remove(label); + } + /// Updates the asset server by inserting the texture provided at the location of the handle, /// and removing the old texture (by returning it back to you). pub fn update_texture(&mut self, handle: Handle<Texture>, texture: Texture) -> Option<Texture> { @@ -64,6 +64,7 @@ impl<T: bytemuck::Pod> UniformBuffer<T> { } pub fn write(&self, queue: &wgpu::Queue, value: &T) { + puffin::profile_function!(&self.label); queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value)); } @@ -106,6 +107,7 @@ impl<T: bytemuck::Pod> StorageBuffer<T> { } pub fn write(&self, queue: &wgpu::Queue, value: &T) { + puffin::profile_function!(self.label()); queue.write_buffer(&self.buffer, 0, bytemuck::bytes_of(value)); } @@ -143,6 +145,7 @@ impl<T: bytemuck::Pod> ResizableBuffer<T> { } pub fn write(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, data: &[T]) { + puffin::profile_function!(&self.label); if data.is_empty() { return; } @@ -231,11 +231,13 @@ impl Camera { } pub fn update(&mut self, graphics: Arc<SharedGraphicsContext>) { + puffin::profile_function!(); self.update_view_proj(); self.buffer.write(&graphics.queue, &self.uniform); } pub fn update_view_proj(&mut self) { + puffin::profile_function!(); let mut uniform = self.uniform; uniform.update(self); self.uniform = uniform; @@ -27,6 +27,7 @@ impl EguiRenderer { msaa_samples: u32, window: &Window, ) -> EguiRenderer { + puffin::profile_function!(); let egui_context = Context::default(); let egui_state = egui_winit::State::new( @@ -75,6 +76,7 @@ impl EguiRenderer { window_surface_view: &TextureView, screen_descriptor: ScreenDescriptor, ) { + puffin::profile_function!(); if !self.frame_started { panic!("begin_frame must be called before end_frame_and_draw can be called!"); } @@ -9,14 +9,15 @@ use std::{ }; use crate::{ - asset::{ASSET_REGISTRY, AssetHandle, AssetKind, AssetRegistry}, + asset::{ASSET_REGISTRY, AssetRegistry}, graphics::{Instance, SharedGraphicsContext}, - model::{LoadedModel, MODEL_CACHE, Model, ModelId}, + model::{Model}, utils::ResourceReference, texture::Texture, }; use anyhow::anyhow; use dropbear_macro::SerializableComponent; +use crate::asset::Handle; /// A type of transform that is attached to all entities. It contains the local and world transforms. #[derive(Default, Debug, Deserialize, Serialize, Copy, PartialEq, Clone, SerializableComponent)] @@ -166,87 +167,48 @@ impl Transform { /// It includes the instances as well as a handle. The reason for a handle is so the model being rendered can be swapped /// to something else without deleting the entire renderer. Also saves memory by rendering anything that has been loaded. pub struct MeshRenderer { - handle: LoadedModel, - pub instance: Instance, - pub previous_matrix: DMat4, - pub is_selected: bool, - pub material_overrides: Vec<MaterialOverride>, - original_material_snapshots: HashMap<String, MaterialSnapshot>, - texture_identifier_cache: HashMap<String, String>, import_scale: f32, -} - -#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] -pub struct MaterialOverride { - pub target_material: String, - pub source_model: ResourceReference, - pub source_material: String, -} -#[derive(Clone)] -struct MaterialSnapshot { - diffuse: Texture, - normal: Texture, - bind_group: wgpu::BindGroup, - texture_tag: Option<String>, + handle: Handle<Model>, + instance: Instance, + previous_matrix: DMat4, + texture_override: Option<Handle<Texture>>, } impl MeshRenderer { + pub fn from_handle(model: Handle<Model>) -> Self { + Self { + handle: model, + instance: Instance::default(), + previous_matrix: DMat4::IDENTITY, + import_scale: 1.0, + texture_override: None, + } + } + pub async fn from_path( graphics: Arc<SharedGraphicsContext>, path: impl AsRef<Path>, label: Option<&str>, ) -> anyhow::Result<Self> { let path = path.as_ref().to_path_buf(); - let handle = Model::load(graphics, &path, label, None).await?; - Ok(Self::from_handle(handle)) - } - - /// Creates a new [`MeshRenderer`] instance from a [`LoadedModel`] handle with an explicit per-renderer import scale. - pub fn from_handle_with_import_scale(handle: LoadedModel, import_scale: f32) -> Self { - Self { + let handle = Model::load_from_memory_raw( + graphics.clone(), + std::fs::read(path)?, + label, + ASSET_REGISTRY.clone(), + ).await?; + Ok(Self { handle, - instance: Instance::new(DVec3::ZERO, DQuat::IDENTITY, DVec3::ONE), + instance: Instance::default(), + import_scale: 1.0, previous_matrix: DMat4::IDENTITY, - is_selected: false, - material_overrides: Vec::new(), - original_material_snapshots: HashMap::new(), - texture_identifier_cache: HashMap::new(), - import_scale, - } - } - - /// Creates a new [`MeshRenderer`] instance from a [`LoadedModel`] handle - pub fn from_handle(handle: LoadedModel) -> Self { - Self::from_handle_with_import_scale(handle, 1.0) - } - - pub fn model(&self) -> Arc<Model> { - self.handle.get() - } - - pub fn model_id(&self) -> ModelId { - self.handle.id() - } - - pub fn asset_handle(&self) -> AssetHandle { - self.handle.asset_handle() - } - - pub fn handle(&self) -> &LoadedModel { - &self.handle - } - - pub fn handle_mut(&mut self) -> &mut LoadedModel { - &mut self.handle - } - - pub fn make_model_mut(&mut self) -> &mut Model { - self.handle.make_mut() + texture_override: None, + }) } pub fn update(&mut self, transform: &Transform) { - // Import scaling is per-renderer and should not mutate shared model buffers. + puffin::profile_function!(); let scale = transform.scale * glam::DVec3::splat(self.import_scale as f64); let current_matrix = DMat4::from_scale_rotation_translation( scale, @@ -259,376 +221,56 @@ impl MeshRenderer { } } - /// Swaps the currently loaded model for that renderer by the provided [`LoadedModel`] - pub fn set_handle(&mut self, handle: LoadedModel) { - self.set_handle_raw(handle); - } - - pub fn set_handle_raw(&mut self, handle: LoadedModel) { - self.handle = handle; - self.material_overrides.clear(); - self.original_material_snapshots.clear(); - self.texture_identifier_cache.clear(); - } - - /// Swaps the currently loaded model for that renderer by the provided [`AssetHandle`] - /// - /// Returns an error if the assethandle provided is not in the model registry. - pub fn set_asset_handle(&mut self, handle: AssetHandle) -> anyhow::Result<()> { - if !ASSET_REGISTRY.contains_handle(handle) { - return Err(anyhow!( - "Asset handle {} is not registered with the asset registry", - handle.raw() - )); - } - - if !ASSET_REGISTRY.is_handle_kind(handle, AssetKind::Model) { - return Err(anyhow!( - "Asset handle {} does not refer to a model asset", - handle.raw() - )); - } - - let model = ASSET_REGISTRY - .get_model(handle) - .ok_or_else(|| anyhow!("Model handle {} not found", handle.raw()))?; - - self.set_handle_raw(LoadedModel::from_registered(handle, model)); - Ok(()) - } - - /// Swaps the loaded model to a different one by using an AssetHandle. - /// - /// The main difference between [`MeshRenderer::set_asset_handle`] and - /// [`MeshRenderer::set_asset_handle_raw`] is that it does not use any static variables - /// (like [`ASSET_REGISTRY`]), instead allowing for the registry to be manually provided. - pub fn set_asset_handle_raw( - &mut self, - registry: &AssetRegistry, - handle: AssetHandle, - ) -> anyhow::Result<()> { - if !registry.contains_handle(handle) { - return Err(anyhow!( - "Asset handle {} is not registered with the asset registry", - handle.raw() - )); - } - - if !registry.is_handle_kind(handle, AssetKind::Model) { - return Err(anyhow!( - "Asset handle {} does not refer to a model asset", - handle.raw() - )); - } - - let model = registry - .get_model(handle) - .ok_or_else(|| anyhow!("Model handle {} not found", handle.raw()))?; - - self.set_handle_raw(LoadedModel::from_registered(handle, model)); - Ok(()) - } - - pub fn uses_model_handle(&self, handle: AssetHandle) -> bool { - self.asset_handle() == handle - } - - pub fn uses_model_reference(&self, reference: &ResourceReference) -> bool { - self.handle().matches_resource(reference) - } - - pub fn contains_material_handle(&self, handle: AssetHandle) -> bool { - self.handle().contains_material_handle(handle) - } - - pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool { - self.handle().contains_material_reference(reference) - } - - pub fn material_handle(&self, material_name: &str) -> Option<AssetHandle> { - self.material_handle_raw(&ASSET_REGISTRY, material_name) - } - - pub fn collect_all_material_handles_raw( - &self, - registry: &AssetRegistry, - ) -> Vec<AssetHandle> { - let model = self.model(); - let model_id = self.model_id(); - - model - .materials - .iter() - .filter_map(|material| { - registry.material_handle(model_id, &material.name) - }) - .collect() - } - - pub fn collect_all_material_handles(&self) -> Vec<AssetHandle> { - self.collect_all_material_handles_raw(&ASSET_REGISTRY) - } - - pub fn material_handle_raw( - &self, - registry: &AssetRegistry, - material_name: &str, - ) -> Option<AssetHandle> { - registry.material_handle(self.model_id(), material_name) - } - - pub fn mesh_handle(&self, mesh_name: &str) -> Option<AssetHandle> { - self.mesh_handle_raw(&ASSET_REGISTRY, mesh_name) - } - - pub fn mesh_handle_raw( - &self, - registry: &AssetRegistry, - mesh_name: &str, - ) -> Option<AssetHandle> { - registry.mesh_handle(self.model_id(), mesh_name) - } - - pub fn apply_material_override( - &mut self, - target_material: &str, - source_model: ResourceReference, - source_material: &str, - ) -> anyhow::Result<()> { - self.apply_material_override_raw( - &ASSET_REGISTRY, - LazyLock::force(&MODEL_CACHE), - target_material, - source_model, - source_material, - ) - } - - pub fn apply_material_override_raw( - &mut self, - registry: &AssetRegistry, - model_cache: &Mutex<HashMap<String, Arc<Model>>>, - target_material: &str, - source_model: ResourceReference, - source_material: &str, - ) -> anyhow::Result<()> { - let snapshot_entry = { - let current_model = self.model(); - let original = current_model - .materials - .iter() - .find(|mat| mat.name == target_material) - .ok_or_else(|| { - anyhow!( - "Target material '{}' does not exist on model '{}'", - target_material, - current_model.label - ) - })?; - - MaterialSnapshot { - diffuse: original.diffuse_texture.clone(), - normal: original.normal_texture.clone(), - bind_group: original.bind_group.clone(), - texture_tag: original.texture_tag.clone(), - } - }; - - self.original_material_snapshots - .entry(target_material.to_string()) - .or_insert(snapshot_entry); - - let source_reference = registry - .model_handle_from_reference(&source_model) - .ok_or_else(|| { - anyhow!( - "Source model {:?} is not registered in the asset registry", - source_model - ) - })?; - - let source_model_arc = registry.get_model(source_reference).ok_or_else(|| { - anyhow!( - "Unable to fetch model handle {:?} from registry", - source_reference - ) - })?; - - let material = source_model_arc - .materials - .iter() - .find(|mat| mat.name == source_material) - .ok_or_else(|| { - anyhow!( - "Material '{}' does not exist on source model {:?}", - source_material, - source_model - ) - })?; - - { - let model = self.make_model_mut(); - if !model.set_material_texture( - target_material, - material.diffuse_texture.clone(), - material.normal_texture.clone(), - material.bind_group.clone(), - material.texture_tag.clone(), - ) { - anyhow::bail!( - "Target material '{}' does not exist on model '{}'", - target_material, - model.label - ); - } - } - - let original_reference = self.model().path.clone(); - let is_default = original_reference == source_model && target_material == source_material; - - self.material_overrides - .retain(|entry| entry.target_material != target_material); - - if !is_default { - self.material_overrides.push(MaterialOverride { - target_material: target_material.to_string(), - source_model, - source_material: source_material.to_string(), - }); - } else { - self.original_material_snapshots.remove(target_material); - self.clear_material_override(target_material); - } - - // ensure downstream caches observe the newly applied material state - self.handle.refresh_registry_raw(registry); - - self.refresh_model_cache_with(model_cache); - - Ok(()) - } - - pub fn material_overrides(&self) -> &[MaterialOverride] { - &self.material_overrides - } - - pub fn clear_texture_identifier_cache(&mut self) { - self.texture_identifier_cache.clear(); - } - - pub fn register_texture_identifier(&mut self, identifier: String, material_name: String) { - match self.texture_identifier_cache.entry(identifier) { - Entry::Occupied(_) => {} - Entry::Vacant(slot) => { - slot.insert(material_name); - } - } - } - - pub fn resolve_texture_identifier(&self, identifier: &str) -> Option<&str> { - self.texture_identifier_cache - .get(identifier) - .map(|value| value.as_str()) - } - - pub fn sync_asset_registry(&mut self) { - self.handle.refresh_registry_raw(&ASSET_REGISTRY); - self.refresh_model_cache_with(LazyLock::force(&MODEL_CACHE)); - } - - pub fn clear_material_override(&mut self, target_material: &str) { - self.material_overrides - .retain(|entry| entry.target_material != target_material); - } - - pub fn restore_original_material(&mut self, target_material: &str) -> anyhow::Result<()> { - self.restore_original_material_raw( - target_material, - &ASSET_REGISTRY, - LazyLock::force(&MODEL_CACHE), - ) - } - - pub fn restore_original_material_raw( - &mut self, - target_material: &str, - registry: &AssetRegistry, - model_cache: &Mutex<HashMap<String, Arc<Model>>>, - ) -> anyhow::Result<()> { - let snapshot = self - .original_material_snapshots - .get(target_material) - .cloned(); - - self.clear_material_override(target_material); - - if let Some(snapshot) = snapshot { - let model = self.make_model_mut(); - if !model.set_material_texture( - target_material, - snapshot.diffuse.clone(), - snapshot.normal.clone(), - snapshot.bind_group.clone(), - snapshot.texture_tag.clone(), - ) { - anyhow::bail!( - "Target material '{}' does not exist on model '{}'", - target_material, - model.label - ); - } - - if snapshot.texture_tag.is_none() { - let _ = model.clear_material_texture_tag(target_material); - } - - self.original_material_snapshots.remove(target_material); - } - - self.handle.refresh_registry_raw(registry); - self.refresh_model_cache_with(model_cache); - - Ok(()) - } - - fn refresh_model_cache_with(&self, cache: &Mutex<HashMap<String, Arc<Model>>>) { - let mut guard = cache.lock(); - self.refresh_model_cache_raw(&mut guard); - } - - fn refresh_model_cache_raw(&self, cache: &mut HashMap<String, Arc<Model>>) { - let current = self.handle.get(); - let keys: Vec<String> = cache - .iter() - .filter_map(|(key, model)| (model.id == current.id).then(|| key.clone())) - .collect(); - - for key in keys { - cache.insert(key, Arc::clone(¤t)); - } - } - - pub fn import_scale(&self) -> f32 { - self.import_scale - } - pub fn set_import_scale(&mut self, scale: f32) { self.import_scale = scale; } - // Backwards-compat helper names (kept for now). - pub fn effective_import_scale(&self) -> f32 { - self.import_scale - } - - pub fn custom_import_scale(&self) -> Option<f32> { - Some(self.import_scale) + pub fn set_model(&mut self, model: Handle<Model>) { + self.handle = model; + } + + pub fn model(&self) -> Handle<Model> { + self.handle + } + + pub fn set_texture_override(&mut self, texture: Handle<Texture>) { + self.texture_override = Some(texture); + } + + pub fn is_texture_attached(&self, texture: Handle<Texture>) -> bool { + let registry = ASSET_REGISTRY.read(); + + if let Some(model) = registry.get_model(self.handle) { + for material in &model.materials { + if material.diffuse_texture.hash == Some(texture.id) { + return true; + } + if material.normal_texture.hash == Some(texture.id) { + return true; + } + if let Some(emissive) = &material.emissive_texture { + if emissive.hash == Some(texture.id) { + return true; + } + } + if let Some(mr) = &material.metallic_roughness_texture { + if mr.hash == Some(texture.id) { + return true; + } + } + if let Some(occ) = &material.occlusion_texture { + if occ.hash == Some(texture.id) { + return true; + } + } + } + } + + false } - pub fn set_custom_import_scale(&mut self, scale: Option<f32>) { - if let Some(scale) = scale { - self.import_scale = scale; - } + pub fn reset_texture_override(&mut self) { + self.texture_override = None; } } @@ -31,7 +31,6 @@ pub struct SharedGraphicsContext { pub egui_renderer: Arc<Mutex<EguiRenderer>>, pub texture_id: Arc<TextureId>, pub future_queue: Arc<FutureQueue>, - pub supports_storage: bool, pub mipmapper: Arc<MipMapper>, pub hdr: Arc<RwLock<HdrPipeline>>, // pub yakui_renderer: Arc<Mutex<yakui_wgpu::YakuiWgpu>>, @@ -76,7 +75,6 @@ impl SharedGraphicsContext { texture_id: state.texture_id.clone(), surface: state.surface.clone(), surface_format: state.surface_format, - supports_storage: state.supports_storage, mipmapper: state.mipmapper.clone(), hdr: state.hdr.clone(), // yakui_renderer: state.yakui_renderer.clone(), @@ -150,46 +148,46 @@ impl InstanceRaw { // model_matrix_0 wgpu::VertexAttribute { offset: 0, - shader_location: 5, + shader_location: 8, format: wgpu::VertexFormat::Float32x4, }, // model_matrix_1 wgpu::VertexAttribute { offset: size_of::<[f32; 4]>() as wgpu::BufferAddress, - shader_location: 6, + shader_location: 9, format: wgpu::VertexFormat::Float32x4, }, // model_matrix_2 wgpu::VertexAttribute { offset: size_of::<[f32; 8]>() as wgpu::BufferAddress, - shader_location: 7, + shader_location: 10, format: wgpu::VertexFormat::Float32x4, }, // model_matrix_3 wgpu::VertexAttribute { offset: size_of::<[f32; 12]>() as wgpu::BufferAddress, - shader_location: 8, + shader_location: 11, format: wgpu::VertexFormat::Float32x4, }, // normal_matrix_0 wgpu::VertexAttribute { offset: size_of::<[f32; 16]>() as wgpu::BufferAddress, - shader_location: 9, + shader_location: 12, format: wgpu::VertexFormat::Float32x3, }, // normal_matrix_1 wgpu::VertexAttribute { offset: size_of::<[f32; 19]>() as wgpu::BufferAddress, - shader_location: 10, + shader_location: 13, format: wgpu::VertexFormat::Float32x3, }, // normal_matrix_2 wgpu::VertexAttribute { offset: size_of::<[f32; 22]>() as wgpu::BufferAddress, - shader_location: 11, + shader_location: 14, format: wgpu::VertexFormat::Float32x3, }, ], @@ -231,6 +229,7 @@ impl CommandEncoder { /// /// Panics if an unwinding error is caught, or just returns the error as normal. pub fn submit(self) -> anyhow::Result<()> { + puffin::profile_function!(); let command_buffer = self.inner.finish(); match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { @@ -196,6 +196,7 @@ impl Manager { } pub fn update(&mut self, gilrs: &mut Gilrs) { + puffin::profile_function!(); self.just_pressed_keys.clear(); self.just_released_keys.clear(); self.just_pressed_mouse_buttons.clear(); @@ -259,6 +260,7 @@ impl Manager { } pub fn handle_controller_event(&mut self, event: gilrs::Event) { + puffin::profile_function!(); for (name, handler) in self.controller_handlers.iter_mut() { if self.active_handlers.contains(name) { match event.event { @@ -299,6 +301,7 @@ impl Manager { } pub fn poll_controllers(&mut self, gilrs: &mut Gilrs) { + puffin::profile_function!(); while let Some(event) = gilrs.next_event() { self.handle_controller_event(event); } @@ -20,6 +20,7 @@ pub mod pipelines; pub mod mipmap; pub mod sky; pub mod features; +pub mod animation; features! { pub mod build { @@ -28,6 +29,12 @@ features! { } } +features! { + pub mod graphics_features { + const SupportsStorage = 0b00000001 + } +} + pub static WGPU_BACKEND: OnceLock<String> = OnceLock::new(); pub const PHYSICS_STEP_RATE: u32 = 120; const MAX_PHYSICS_STEPS_PER_FRAME: usize = 4; @@ -76,13 +83,13 @@ use crate::scene::Scene; pub struct BindGroupLayouts { pub shader_globals_bind_group_layout: BindGroupLayout, - pub texture_bind_layout: BindGroupLayout, - pub material_tint_bind_layout: BindGroupLayout, + pub material_bind_layout: BindGroupLayout, pub camera_bind_group_layout: BindGroupLayout, pub light_bind_group_layout: BindGroupLayout, pub light_array_bind_group_layout: BindGroupLayout, pub light_cube_bind_group_layout: BindGroupLayout, pub environment_bind_group_layout: BindGroupLayout, + pub skinning_bind_group_layout: BindGroupLayout, } /// The backend information, such as the device, queue, config, surface, renderer, window and more. @@ -90,7 +97,6 @@ pub struct State { // keep top for drop order pub window: Arc<Window>, pub instance: Arc<Instance>, - pub supports_storage: bool, pub surface: Arc<Surface<'static>>, pub surface_format: TextureFormat, @@ -115,25 +121,6 @@ pub struct State { } impl State { - /// As defined in `shaders.wgsl` as - /// ``` - /// @group(3) @binding(0) - /// var<uniform> u_material: MaterialUniform; - /// ``` - const MATERIAL_BIND_GROUP_LAYOUT: wgpu::BindGroupLayoutDescriptor<'_> = wgpu::BindGroupLayoutDescriptor { - entries: &[wgpu::BindGroupLayoutEntry { - binding: 0, - visibility: wgpu::ShaderStages::FRAGMENT, - ty: wgpu::BindingType::Buffer { - ty: wgpu::BufferBindingType::Uniform, - has_dynamic_offset: false, - min_binding_size: None, - }, - count: None, - }], - label: Some("material bind group layout"), - }; - /// Asynchronously initialised the state and sets up the backend and surface for wgpu to render to. pub async fn new(window: Arc<Window>, instance: Arc<Instance>, future_queue: Arc<FutureQueue>) -> anyhow::Result<Self> { let title = window.title(); @@ -178,8 +165,12 @@ impl State { .flags .contains(wgpu::DownlevelFlags::VERTEX_STORAGE) && device.limits().max_storage_buffers_per_shader_stage > 0; + + if supports_storage_resources { + graphics_features::enable(graphics_features::SupportsStorage); + } - log::debug!("graphics device {} support storage resources", if !supports_storage_resources { "does not" } else { "does" }); + log::debug!("graphics device {} support storage resources", if !supports_storage_resources { "DOES NOT" } else { "DOES" }); if WGPU_BACKEND.get().is_none() { let info = adapter.get_info(); @@ -293,11 +284,61 @@ Hardware: label: Some("Per-Light Layout"), }); - // shaders/shader.wgsl - @group(0) - let texture_bind_group_layout = - device.create_bind_group_layout(&texture::TEXTURE_BIND_GROUP_LAYOUT); - // shaders/shader.wgsl - @group(2) + let material_bind_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("material_bind_layout"), + entries: &[ + // t_diffuse + wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + // s_diffuse + wgpu::BindGroupLayoutEntry { + binding: 1, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // t_normal + wgpu::BindGroupLayoutEntry { + binding: 2, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Texture { + multisampled: false, + view_dimension: wgpu::TextureViewDimension::D2, + sample_type: wgpu::TextureSampleType::Float { filterable: true }, + }, + count: None, + }, + // s_normal + wgpu::BindGroupLayoutEntry { + binding: 3, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering), + count: None, + }, + // u_material + wgpu::BindGroupLayoutEntry { + binding: 4, + visibility: wgpu::ShaderStages::FRAGMENT, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Uniform, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }, + ], + }); + + // shaders/shader.wgsl - @group(1) let light_array_bind_group_layout = device.create_bind_group_layout( &wgpu::BindGroupLayoutDescriptor { entries: &[ @@ -306,13 +347,7 @@ Hardware: binding: 0, visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT, ty: wgpu::BindingType::Buffer { - ty: if supports_storage_resources { - // s_light_array - wgpu::BufferBindingType::Storage { read_only: true } - } else { - // u_light_array - wgpu::BufferBindingType::Uniform - }, + ty: wgpu::BufferBindingType::Storage { read_only: true }, has_dynamic_offset: false, min_binding_size: None, }, @@ -324,10 +359,6 @@ Hardware: ); // shaders/shader.wgsl - @group(3) - let material_tint_bind_group_layout = - device.create_bind_group_layout(&Self::MATERIAL_BIND_GROUP_LAYOUT); - - // shaders/shader.wgsl - @group(4) let shader_globals_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { label: Some("shader.wgsl globals bind group layout"), entries: &[ @@ -369,7 +400,7 @@ Hardware: let environment_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { - label: Some("environment_layout"), + label: Some("environment bind group layout"), entries: &[ wgpu::BindGroupLayoutEntry { binding: 0, @@ -390,6 +421,20 @@ Hardware: ], }); + let skinning_bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { + label: Some("skinning bind group layout"), + entries: &[wgpu::BindGroupLayoutEntry { + binding: 0, + visibility: wgpu::ShaderStages::VERTEX, + ty: wgpu::BindingType::Buffer { + ty: wgpu::BufferBindingType::Storage { read_only: true }, + has_dynamic_offset: false, + min_binding_size: None, + }, + count: None, + }], + }); + let result = Self { surface: Arc::new(surface), surface_format, @@ -409,15 +454,14 @@ Hardware: scene_manager: scene::Manager::new(), layouts: Arc::new(BindGroupLayouts { shader_globals_bind_group_layout, - texture_bind_layout: texture_bind_group_layout, - material_tint_bind_layout: material_tint_bind_group_layout, + material_bind_layout, camera_bind_group_layout, light_bind_group_layout, light_array_bind_group_layout, light_cube_bind_group_layout, environment_bind_group_layout, + skinning_bind_group_layout, }), - supports_storage: supports_storage_resources, // yakui_renderer, // yakui_texture, hdr, @@ -462,6 +506,7 @@ Hardware: event_loop: &ActiveEventLoop, graphics: Arc<SharedGraphicsContext>, ) -> anyhow::Result<Vec<scene::SceneCommand>> { + puffin::profile_function!(); if !self.is_surface_configured { return Ok(Vec::new()); } @@ -511,6 +556,7 @@ Hardware: }); { // ensures clearing of the encoder is done correctly. + puffin::profile_scope!("surface clear"); let mut encoder = CommandEncoder::new(graphics.clone(), Some("surface clear render encoder")); { @@ -916,6 +962,12 @@ impl App { fn new(app_data: AppInfo, future_queue: Option<Arc<FutureQueue>>) -> Self { if build::is_enabled(build::Debug) { puffin::set_scopes_on(true); + + if let Err(e) = puffin_http::Server::new("127.0.0.1:8585") { + log::error!("Unable to start puffin http server: {}", e); + } else { + log::info!("Started puffin http server at \"127.0.0.1:8585\""); + }; } let instance = Arc::new(Instance::new(&wgpu::InstanceDescriptor { @@ -953,6 +1005,8 @@ impl App { /// Creates a new window and adds it to its internal window manager (its really just a hashmap). pub fn create_window(&mut self, event_loop: &ActiveEventLoop, attribs: WindowAttributes) -> anyhow::Result<WindowId> { + puffin::profile_function!("load wgpu state"); + let window = Arc::new( event_loop.create_window(attribs)? ); @@ -1092,7 +1146,7 @@ impl ApplicationHandler for App { puffin::GlobalProfiler::lock().new_frame(); let frame_start = Instant::now(); - + let active_handlers = state.scene_manager.get_active_input_handlers(); self.input_manager.set_active_handlers(active_handlers); @@ -12,7 +12,9 @@ use glam::{DMat4, DVec3}; use std::fmt::{Display, Formatter}; use std::sync::Arc; use wgpu::{BindGroup}; -use crate::asset::ASSET_REGISTRY; +use crate::asset::{Handle, ASSET_REGISTRY}; +use crate::model::Material; +use crate::procedural::{ProcObj, ProcedurallyGeneratedObject}; pub const MAX_LIGHTS: usize = 10; @@ -241,7 +243,7 @@ impl LightComponent { #[derive(Clone)] pub struct Light { pub uniform: LightUniform, - pub cube_model: Arc<Model>, + pub cube_model: Handle<Model>, pub label: String, pub buffer: UniformBuffer<LightUniform>, pub bind_group: BindGroup, @@ -271,6 +273,7 @@ impl Light { transform: Transform, label: Option<&str>, ) -> Self { + puffin::profile_function!(); let forward = DVec3::new(0.0, 0.0, -1.0); let direction = transform.rotation * forward; @@ -289,16 +292,13 @@ impl Light { log::trace!("Created new light uniform"); - let cube_model = Model::load_from_memory_raw( - graphics.clone(), - include_bytes!("../../../resources/models/cube.glb").to_vec(), - label, - ASSET_REGISTRY.clone(), - None - ) - .await - .expect("failed to load light cube model") - .get(); + let cube_model = ProcedurallyGeneratedObject::cuboid(DVec3::ONE) + .build_model( + graphics.clone(), + None, + None, + ASSET_REGISTRY.clone() + ); let label_str = label.unwrap_or("Light").to_string(); @@ -339,6 +339,7 @@ impl Light { } pub fn update(&mut self, graphics: &SharedGraphicsContext, light: &mut LightComponent, transform: &Transform) { + puffin::profile_function!(); self.uniform.position = dvec3_to_uniform_array(transform.position); let forward = DVec3::new(0.0, 0.0, -1.0); @@ -361,8 +362,8 @@ impl Light { &self.uniform } - pub fn model(&self) -> &Model { - &self.cube_model + pub fn model(&self) -> Handle<Model> { + self.cube_model } pub fn label(&self) -> &str { @@ -12,6 +12,7 @@ pub struct MipMapper { impl MipMapper { pub fn new(device: &wgpu::Device) -> Self { + puffin::profile_function!(); let blit_shader = device.create_shader_module(slank::CompiledSlangShader::from_bytes( "mipmap blit_shader", include_slang!("blit_shader") @@ -126,6 +127,7 @@ impl MipMapper { queue: &wgpu::Queue, texture: &Texture, ) -> anyhow::Result<()> { + puffin::profile_function!(); let texture = &texture.texture; match texture.format() { @@ -273,6 +275,7 @@ impl MipMapper { queue: &wgpu::Queue, texture: &Texture, ) -> anyhow::Result<()> { + puffin::profile_function!(); let texture = &texture.texture; match texture.format() { @@ -5,7 +5,7 @@ use crate::{ utils::{ResourceReference}, texture::{Texture, TextureWrapMode} }; -use image::GenericImageView; +// use image::GenericImageView; use parking_lot::{Mutex, RwLock}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; @@ -17,15 +17,29 @@ use std::time::Instant; use std::{mem, ops::Range, path::PathBuf}; use gltf::image::Format; use gltf::texture::MinFilter; +use puffin::profile_scope; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup}; #[derive(Clone)] pub struct Model { + pub(crate) hash: u64, // also the id related to the handle pub label: String, - pub(crate) hash: u64, pub path: ResourceReference, pub meshes: Vec<Mesh>, pub materials: Vec<Material>, + pub skins: Vec<Skin>, + pub animations: Vec<Animation>, + pub nodes: Vec<Node>, +} + +#[derive(Clone)] +pub struct Mesh { + pub name: String, + pub vertex_buffer: wgpu::Buffer, + pub index_buffer: wgpu::Buffer, + pub num_elements: u32, + pub material: usize, + pub vertices: Vec<ModelVertex>, } #[derive(Clone)] @@ -45,7 +59,6 @@ pub struct Material { pub normal_scale: f32, pub uv_tiling: [f32; 2], pub tint_buffer: UniformBuffer<MaterialUniform>, - pub tint_bind_group: wgpu::BindGroup, pub texture_tag: Option<String>, pub wrap_mode: TextureWrapMode, pub emissive_texture: Option<Texture>, @@ -53,6 +66,77 @@ pub struct Material { pub occlusion_texture: Option<Texture>, } +/// Represents a node in the scene graph (can be a joint/bone or a mesh) +#[derive(Clone, Debug)] +pub struct Node { + pub name: String, + pub parent: Option<usize>, + pub children: Vec<usize>, + pub transform: NodeTransform, +} + +/// Local transform of a node relative to its parent +#[derive(Clone, Debug)] +pub struct NodeTransform { + pub translation: glam::Vec3, + pub rotation: glam::Quat, + pub scale: glam::Vec3, +} + +impl NodeTransform { + pub fn to_matrix(&self) -> glam::Mat4 { + glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) + } + + pub fn identity() -> Self { + Self { + translation: glam::Vec3::ZERO, + rotation: glam::Quat::IDENTITY, + scale: glam::Vec3::ONE, + } + } +} + +/// A skin defines how a mesh is bound to a skeleton +#[derive(Clone)] +pub struct Skin { + pub name: String, + /// Indices of joints (nodes) in the Model's nodes array + pub joints: Vec<usize>, + /// Inverse bind matrices - one per joint + pub inverse_bind_matrices: Vec<glam::Mat4>, + /// Optional root joint index + pub skeleton_root: Option<usize>, +} + +/// An animation that can be played on a skeleton +#[derive(Clone)] +pub struct Animation { + pub name: String, + pub channels: Vec<AnimationChannel>, + pub duration: f32, +} + +/// Describes how an animation affects a specific node +#[derive(Clone)] +pub struct AnimationChannel { + /// Target node index in the Model's nodes array + pub target_node: usize, + /// Keyframe times + pub times: Vec<f32>, + /// Animation data + pub values: ChannelValues, + /// Interpolation method + pub interpolation: AnimationInterpolation, +} + +#[derive(Clone)] +pub enum ChannelValues { + Translations(Vec<glam::Vec3>), + Rotations(Vec<glam::Quat>), + Scales(Vec<glam::Vec3>), +} + impl Material { pub fn new( graphics: Arc<SharedGraphicsContext>, @@ -62,6 +146,7 @@ impl Material { tint: [f32; 4], texture_tag: Option<String>, ) -> Self { + puffin::profile_function!(); let name = name.into(); let uv_tiling = [1.0, 1.0]; @@ -80,16 +165,8 @@ impl Material { let tint_buffer = UniformBuffer::new(&graphics.device, "material_tint_uniform"); tint_buffer.write(&graphics.queue, &uniform); - let tint_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { - layout: &graphics.layouts.material_tint_bind_layout, - entries: &[wgpu::BindGroupEntry { - binding: 0, - resource: tint_buffer.buffer().as_entire_binding(), - }], - label: Some("material tint bind group"), - }); - - let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &name); + + let bind_group = Self::create_bind_group(&graphics, &diffuse_texture, &normal_texture, &tint_buffer, &name); Self { name, @@ -107,7 +184,6 @@ impl Material { normal_scale: 1.0, uv_tiling, tint_buffer, - tint_bind_group, texture_tag, wrap_mode: TextureWrapMode::Repeat, emissive_texture: None, @@ -120,11 +196,13 @@ impl Material { graphics: &SharedGraphicsContext, diffuse: &Texture, normal: &Texture, + uniform_buffer: &UniformBuffer<MaterialUniform>, name: &str, ) -> BindGroup { + puffin::profile_function!(); graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some(format!("{} texture bind group", name).as_str()), - layout: &graphics.layouts.texture_bind_layout, + layout: &graphics.layouts.material_bind_layout, entries: &[ wgpu::BindGroupEntry { binding: 0, @@ -142,6 +220,10 @@ impl Material { binding: 3, resource: wgpu::BindingResource::Sampler(&normal.sampler), }, + wgpu::BindGroupEntry { + binding: 4, + resource: uniform_buffer.buffer().as_entire_binding(), + }, ], }) } @@ -164,131 +246,29 @@ impl Material { } } -#[derive(Clone)] -pub struct Mesh { - pub name: String, - pub vertex_buffer: wgpu::Buffer, - pub index_buffer: wgpu::Buffer, - pub num_elements: u32, - pub material: usize, - pub vertices: Vec<ModelVertex>, +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AnimationInterpolation { + /// The animated values are linearly interpolated between keyframes + Linear, + /// The animated values remain constant between keyframes + Step, + /// The animated values are interpolated using a cubic spline + CubicSpline, } -struct GLTFTextureInformation<'a> { - sampler: wgpu::SamplerDescriptor<'a>, +struct GLTFTextureInformation { + sampler: wgpu::SamplerDescriptor<'static>, pixels: Vec<u8>, + width: u32, + height: u32, + #[allow(dead_code)] mip_level_count: u32, + #[allow(dead_code)] format: wgpu::TextureFormat, } -struct GLTFMeshInformation { - name: String, - primitive_index: usize, - material_index: usize, - mode: gltf::mesh::Mode, - positions: Vec<[f32; 3]>, - indices: Vec<u32>, - normals: Vec<[f32; 3]>, - tangents: Vec<[f32; 4]>, - colors: Vec<[f32; 4]>, - joints: Vec<[u16; 4]>, - weights: Vec<[f32; 4]>, - tex_coords0: Vec<[f32; 2]>, - tex_coords1: Vec<[f32; 2]>, -} - -struct GLTFMaterialInformation { - name: String, - diffuse_bytes: Option<Vec<u8>>, - normal_bytes: Option<Vec<u8>>, - emissive_bytes: Option<Vec<u8>>, - metallic_roughness_bytes: Option<Vec<u8>>, - occlusion_bytes: Option<Vec<u8>>, - diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>, - normal_sampler: Option<wgpu::SamplerDescriptor<'static>>, - emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>, - metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>, - occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>, - tint: [f32; 4], - emissive_factor: [f32; 3], - metallic_factor: f32, - roughness_factor: f32, - alpha_mode: gltf::material::AlphaMode, - alpha_cutoff: Option<f32>, - double_sided: bool, - occlusion_strength: f32, - normal_scale: f32, -} - -struct ProcessedMaterialTextures { - name: String, - diffuse: Option<(Vec<u8>, (u32, u32))>, - normal: Option<(Vec<u8>, (u32, u32))>, - emissive: Option<(Vec<u8>, (u32, u32))>, - metallic_roughness: Option<(Vec<u8>, (u32, u32))>, - occlusion: Option<(Vec<u8>, (u32, u32))>, - diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>, - normal_sampler: Option<wgpu::SamplerDescriptor<'static>>, - emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>, - metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>, - occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>, - tint: [f32; 4], - emissive_factor: [f32; 3], - metallic_factor: f32, - roughness_factor: f32, - alpha_mode: gltf::material::AlphaMode, - alpha_cutoff: Option<f32>, - double_sided: bool, - occlusion_strength: f32, - normal_scale: f32, -} - -impl Model { - fn sampler_descriptor_for_texture(texture: &gltf::Texture<'_>) -> wgpu::SamplerDescriptor<'static> { - let sampler = texture.sampler(); - - let mag_filter = match sampler.mag_filter() { - Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest, - _ => wgpu::FilterMode::Linear, - }; - - let (min_filter, mipmap_filter) = match sampler.min_filter() { - Some(MinFilter::Nearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest), - Some(MinFilter::Linear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest), - Some(MinFilter::NearestMipmapNearest) => { - (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest) - } - Some(MinFilter::LinearMipmapNearest) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest), - Some(MinFilter::NearestMipmapLinear) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear), - Some(MinFilter::LinearMipmapLinear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear), - None => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear), - }; - - fn map_wrap(wrap: gltf::texture::WrappingMode) -> wgpu::AddressMode { - match wrap { - gltf::texture::WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, - gltf::texture::WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, - gltf::texture::WrappingMode::Repeat => wgpu::AddressMode::Repeat, - } - } - - wgpu::SamplerDescriptor { - label: None, - address_mode_u: map_wrap(sampler.wrap_s()), - address_mode_v: map_wrap(sampler.wrap_t()), - address_mode_w: wgpu::AddressMode::Repeat, - mag_filter, - min_filter, - mipmap_filter, - lod_min_clamp: 0.0, - lod_max_clamp: 32.0, - compare: None, - anisotropy_clamp: 1, - border_color: None, - } - } - - fn load_texture<'a>(tex: &'a gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation<'a> { +impl GLTFTextureInformation { + fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation { let sampler = tex.sampler(); let mag_filter = match sampler.mag_filter() { @@ -315,7 +295,7 @@ impl Model { } let sampler = wgpu::SamplerDescriptor { - label: Some(tex.name().unwrap_or("Unnamed Texture Sampler")), + label: None, address_mode_u: map_wrap(sampler.wrap_s()), address_mode_v: map_wrap(sampler.wrap_t()), address_mode_w: wgpu::AddressMode::Repeat, @@ -357,34 +337,86 @@ impl Model { mip_level_count, pixels, format, + width, + height, } } +} + +struct GLTFMeshInformation { + name: String, + primitive_index: usize, + material_index: usize, + mode: gltf::mesh::Mode, + positions: Vec<[f32; 3]>, + indices: Vec<u32>, + normals: Vec<[f32; 3]>, + tangents: Vec<[f32; 4]>, + colors: Vec<[f32; 4]>, + joints: Vec<[u16; 4]>, + weights: Vec<[f32; 4]>, + tex_coords0: Vec<[f32; 2]>, + tex_coords1: Vec<[f32; 2]>, +} + +struct GLTFMaterialInformation { + name: String, + diffuse_texture: Option<GLTFTextureInformation>, + normal_texture: Option<GLTFTextureInformation>, + emissive_texture: Option<GLTFTextureInformation>, + metallic_roughness_texture: Option<GLTFTextureInformation>, + occlusion_texture: Option<GLTFTextureInformation>, + tint: [f32; 4], + emissive_factor: [f32; 3], + metallic_factor: f32, + roughness_factor: f32, + alpha_mode: gltf::material::AlphaMode, + alpha_cutoff: Option<f32>, + double_sided: bool, + occlusion_strength: f32, + normal_scale: f32, +} + +struct ProcessedMaterialTextures { + name: String, + diffuse: Option<(Vec<u8>, (u32, u32))>, + normal: Option<(Vec<u8>, (u32, u32))>, + emissive: Option<(Vec<u8>, (u32, u32))>, + metallic_roughness: Option<(Vec<u8>, (u32, u32))>, + occlusion: Option<(Vec<u8>, (u32, u32))>, + diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>, + normal_sampler: Option<wgpu::SamplerDescriptor<'static>>, + emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>, + metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>, + occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>, + tint: [f32; 4], + emissive_factor: [f32; 3], + metallic_factor: f32, + roughness_factor: f32, + alpha_mode: gltf::material::AlphaMode, + alpha_cutoff: Option<f32>, + double_sided: bool, + occlusion_strength: f32, + normal_scale: f32, +} +impl Model { fn load_materials( gltf: &gltf::Document, - buffers: &Vec<gltf::buffer::Data>, + _buffers: &Vec<gltf::buffer::Data>, + images: &Vec<gltf::image::Data>, ) -> Vec<GLTFMaterialInformation> { - let read_texture_bytes = |texture: gltf::Texture<'_>| -> Option<Vec<u8>> { - let image = texture.source(); - match image.source() { - gltf::image::Source::View { view, mime_type: _ } => { - let buffer_data = &buffers[view.buffer().index()]; - let start = view.offset(); - let end = start + view.length(); - Some(buffer_data[start..end].to_vec()) - } - gltf::image::Source::Uri { uri, mime_type: _ } => { - log::warn!("External URI textures not supported: {}", uri); - None - } - } + puffin::profile_function!(); + let process_texture = |texture: gltf::Texture<'_>| -> Option<GLTFTextureInformation> { + puffin::profile_scope!("reading texture bytes", texture.name().unwrap_or("Unnamed Texture")); + Some(GLTFTextureInformation::fetch(&texture, images)) }; let mut material_data = Vec::new(); for material in gltf.materials() { - log::debug!("Processing material: {:?}", material.name()); let material_name = material.name().unwrap_or("Unnamed Material").to_string(); + puffin::profile_scope!("loading material", &material_name); let tint = material.pbr_metallic_roughness().base_color_factor(); let tint = [tint[0], tint[1], tint[2], tint[3]]; @@ -396,38 +428,22 @@ impl Model { let occlusion_texture = material.occlusion_texture(); let emissive_texture = material.emissive_texture(); - let diffuse_bytes = diffuse_texture - .as_ref() - .and_then(|info| read_texture_bytes(info.texture())); - let metallic_roughness_bytes = metallic_roughness_texture - .as_ref() - .and_then(|info| read_texture_bytes(info.texture())); - - let normal_bytes = normal_texture - .as_ref() - .and_then(|info| read_texture_bytes(info.texture())); - let occlusion_bytes = occlusion_texture + let diffuse_texture_info = diffuse_texture .as_ref() - .and_then(|info| read_texture_bytes(info.texture())); - let emissive_bytes = emissive_texture + .and_then(|info| process_texture(info.texture())); + let metallic_roughness_texture_info = metallic_roughness_texture .as_ref() - .and_then(|info| read_texture_bytes(info.texture())); + .and_then(|info| process_texture(info.texture())); - let diffuse_sampler = diffuse_texture - .as_ref() - .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); - let metallic_roughness_sampler = metallic_roughness_texture - .as_ref() - .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); - let normal_sampler = normal_texture + let normal_texture_info = normal_texture .as_ref() - .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); - let occlusion_sampler = occlusion_texture + .and_then(|info| process_texture(info.texture())); + let occlusion_texture_info = occlusion_texture .as_ref() - .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); - let emissive_sampler = emissive_texture + .and_then(|info| process_texture(info.texture())); + let emissive_texture_info = emissive_texture .as_ref() - .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); + .and_then(|info| process_texture(info.texture())); let emissive_factor = material.emissive_factor(); let emissive_factor = [ @@ -451,16 +467,11 @@ impl Model { material_data.push(GLTFMaterialInformation { name: material_name, - diffuse_bytes, - normal_bytes, - emissive_bytes, - metallic_roughness_bytes, - occlusion_bytes, - diffuse_sampler, - normal_sampler, - emissive_sampler, - metallic_roughness_sampler, - occlusion_sampler, + diffuse_texture: diffuse_texture_info, + normal_texture: normal_texture_info, + emissive_texture: emissive_texture_info, + metallic_roughness_texture: metallic_roughness_texture_info, + occlusion_texture: occlusion_texture_info, tint, emissive_factor, metallic_factor, @@ -476,16 +487,11 @@ impl Model { if material_data.is_empty() { material_data.push(GLTFMaterialInformation { name: "Default".to_string(), - diffuse_bytes: None, - normal_bytes: None, - emissive_bytes: None, - metallic_roughness_bytes: None, - occlusion_bytes: None, - diffuse_sampler: None, - normal_sampler: None, - emissive_sampler: None, - metallic_roughness_sampler: None, - occlusion_sampler: None, + diffuse_texture: None, + normal_texture: None, + emissive_texture: None, + metallic_roughness_texture: None, + occlusion_texture: None, tint: [1.0, 1.0, 1.0, 1.0], emissive_factor: [0.0, 0.0, 0.0], metallic_factor: 1.0, @@ -507,8 +513,11 @@ impl Model { mesh_collector: &mut Vec<GLTFMeshInformation>, ) -> anyhow::Result<()> { let mesh_name = mesh.name().unwrap_or("Unnamed Mesh").to_string(); + puffin::profile_function!(&mesh_name); for (primitive_index, primitive) in mesh.primitives().enumerate() { + puffin::profile_scope!("reading primitive", &format!("{}[{}]", &mesh_name, primitive_index)); + let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); let positions: Vec<[f32; 3]> = reader @@ -609,26 +618,221 @@ impl Model { Ok(()) } + fn load_nodes(gltf: &gltf::Document) -> Vec<Node> { + puffin::profile_function!("loading nodes"); + let mut nodes = Vec::new(); + + for node in gltf.nodes() { + profile_scope!("reading node", node.name().unwrap_or("Unnamed Node")); + let (translation, rotation, scale) = node.transform().decomposed(); + + let transform = NodeTransform { + translation: glam::Vec3::from(translation), + rotation: glam::Quat::from_array(rotation), + scale: glam::Vec3::from(scale), + }; + + nodes.push(Node { + name: node.name().unwrap_or("Unnamed Node").to_string(), + parent: None, + children: node.children().map(|n| n.index()).collect(), + transform, + }); + } + + for (node_index, node) in gltf.nodes().enumerate() { + profile_scope!("second pass enumerating children", node.name().unwrap_or("Unnamed Node")); + for child in node.children() { + if let Some(child_node) = nodes.get_mut(child.index()) { + child_node.parent = Some(node_index); + } + } + } + + nodes + } + + fn load_skins(gltf: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec<Skin> { + puffin::profile_function!("loading skins"); + let mut skins = Vec::new(); + + for skin in gltf.skins() { + puffin::profile_scope!("reading skin", skin.name().unwrap_or("Unnamed Skin")); + let joints: Vec<usize> = skin.joints().map(|j| j.index()).collect(); + + let inverse_bind_matrices = if let Some(accessor) = skin.inverse_bind_matrices() { + let view = accessor.view().expect("Accessor must have a buffer view"); + let buffer_data = &buffers[view.buffer().index()]; + let start = view.offset() + accessor.offset(); + let stride = view.stride().unwrap_or(accessor.size()); + + let mut matrices = Vec::with_capacity(accessor.count()); + for i in 0..accessor.count() { + let offset = start + i * stride; + let matrix_bytes = &buffer_data[offset..offset + 64]; + + let mut floats = [0f32; 16]; + for (j, chunk) in matrix_bytes.chunks_exact(4).enumerate() { + floats[j] = f32::from_le_bytes([chunk[0], chunk[1], chunk[2], chunk[3]]); + } + + matrices.push(glam::Mat4::from_cols_array(&floats)); + } + matrices + } else { + vec![glam::Mat4::IDENTITY; joints.len()] + }; + + skins.push(Skin { + name: skin.name().unwrap_or("Unnamed Skin").to_string(), + joints, + inverse_bind_matrices, + skeleton_root: skin.skeleton().map(|n| n.index()), + }); + } + + skins + } + + fn load_animations(gltf: &gltf::Document, buffers: &[gltf::buffer::Data]) -> Vec<Animation> { + puffin::profile_function!("loading animations"); + let mut animations = Vec::new(); + + for animation in gltf.animations() { + puffin::profile_scope!("reading animation", animation.name().unwrap_or("Unnamed Animation")); + let mut channels = Vec::new(); + let mut max_time = 0.0f32; + + for channel in animation.channels() { + let target = channel.target(); + let reader = channel.reader(|buffer| Some(&buffers[buffer.index()])); + let interpolation_mode = channel.sampler().interpolation(); + + let times: Vec<f32> = if let Some(inputs) = reader.read_inputs() { + inputs.collect() + } else { + continue; + }; + + if let Some(&last_time) = times.last() { + max_time = max_time.max(last_time); + } + + let values = match target.property() { + gltf::animation::Property::Translation => { + puffin::profile_scope!("reading translation values"); + if let Some(outputs) = reader.read_outputs() { + match outputs { + gltf::animation::util::ReadOutputs::Translations(iter) => { + let translations: Vec<glam::Vec3> = iter + .map(|t| glam::Vec3::from(t)) + .collect(); + ChannelValues::Translations(translations) + } + _ => continue, + } + } else { + continue; + } + } + gltf::animation::Property::Rotation => { + puffin::profile_scope!("reading rotation values"); + if let Some(outputs) = reader.read_outputs() { + match outputs { + gltf::animation::util::ReadOutputs::Rotations(iter) => { + let rotations: Vec<glam::Quat> = if interpolation_mode == gltf::animation::Interpolation::CubicSpline { + iter.into_f32() + .enumerate() + .map(|(i, r)| { + let q = glam::Quat::from_array(r); + if i % 3 == 1 { + q.normalize() + } else { + q + } + }) + .collect() + } else { + iter.into_f32() + .map(|r| glam::Quat::from_array(r).normalize()) + .collect() + }; + ChannelValues::Rotations(rotations) + } + _ => continue, + } + } else { + continue; + } + } + gltf::animation::Property::Scale => { + puffin::profile_scope!("reading scale values"); + if let Some(outputs) = reader.read_outputs() { + match outputs { + gltf::animation::util::ReadOutputs::Scales(iter) => { + let scales: Vec<glam::Vec3> = iter + .map(|s| glam::Vec3::from(s)) + .collect(); + ChannelValues::Scales(scales) + } + _ => continue, + } + } else { + continue; + } + } + gltf::animation::Property::MorphTargetWeights => { + puffin::profile_scope!("reading morph target weights"); + // Skip morph targets for now + continue; + } + }; + + let interpolation = match channel.sampler().interpolation() { + gltf::animation::Interpolation::Linear => AnimationInterpolation::Linear, + gltf::animation::Interpolation::Step => AnimationInterpolation::Step, + gltf::animation::Interpolation::CubicSpline => AnimationInterpolation::CubicSpline, + }; + + channels.push(AnimationChannel { + target_node: target.node().index(), + times, + values, + interpolation, + }); + } + + animations.push(Animation { + name: animation.name().unwrap_or("Unnamed Animation").to_string(), + channels, + duration: max_time, + }); + } + + animations + } + pub async fn load_from_memory_raw<B>( graphics: Arc<SharedGraphicsContext>, buffer: B, label: Option<&str>, registry: Arc<RwLock<AssetRegistry>>, - import_scale: Option<f64>, ) -> anyhow::Result<Handle<Model>> where B: AsRef<[u8]>, { + puffin::profile_function!(label.unwrap_or("unlabelled model")); let mut registry = registry.write(); let model_label = label.unwrap_or("No named model"); - let hash = if let Some(label) = label { + let hash = { + puffin::profile_scope!("hashing model"); let mut hasher = DefaultHasher::default(); - label.hash(&mut hasher); - hasher.finish() - } else { - let mut hasher = DefaultHasher::default(); - buffer.as_ref().hash(&mut hasher); + if let Some(label) = label { + label.hash(&mut hasher); + } else { + buffer.as_ref().hash(&mut hasher); + }; hasher.finish() }; @@ -636,32 +840,49 @@ impl Model { return Ok(model); } - let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?; + let (gltf, buffers, images) = gltf::import_slice(buffer.as_ref())?; let mut meshes = Vec::new(); for mesh in gltf.meshes() { Self::load_meshes(&mesh, &buffers, &mut meshes)?; } - let material_data = Self::load_materials(&gltf, &buffers); + let nodes = Self::load_nodes(&gltf); + + let skins = Self::load_skins(&gltf, &buffers); + + let animations = Self::load_animations(&gltf, &buffers); + + log::debug!( + "Loaded {} nodes, {} skins, {} animations for model [{:?}]", + nodes.len(), + skins.len(), + animations.len(), + label + ); + + let material_data = Self::load_materials(&gltf, &buffers, &images); - let parallel_start = Instant::now(); let processed_textures: Vec<ProcessedMaterialTextures> = material_data .into_par_iter() .map(|material_info| { - let material_start = Instant::now(); - + puffin::profile_scope!("processing material textures"); let material_name = material_info.name; - let diffuse_bytes = material_info.diffuse_bytes; - let normal_bytes = material_info.normal_bytes; - let emissive_bytes = material_info.emissive_bytes; - let metallic_roughness_bytes = material_info.metallic_roughness_bytes; - let occlusion_bytes = material_info.occlusion_bytes; - let diffuse_sampler = material_info.diffuse_sampler; - let normal_sampler = material_info.normal_sampler; - let emissive_sampler = material_info.emissive_sampler; - let metallic_roughness_sampler = material_info.metallic_roughness_sampler; - let occlusion_sampler = material_info.occlusion_sampler; + + let extract = |info: Option<GLTFTextureInformation>| -> (Option<(Vec<u8>, (u32, u32))>, Option<wgpu::SamplerDescriptor<'static>>) { + if let Some(info) = info { + (Some((info.pixels, (info.width, info.height))), Some(info.sampler)) + } else { + (None, None) + } + }; + + let (processed_diffuse, diffuse_sampler) = extract(material_info.diffuse_texture); + let (processed_normal, normal_sampler) = extract(material_info.normal_texture); + let (processed_emissive, emissive_sampler) = extract(material_info.emissive_texture); + let (processed_metallic_roughness, metallic_roughness_sampler) = extract(material_info.metallic_roughness_texture); + let (processed_occlusion, occlusion_sampler) = extract(material_info.occlusion_texture); + let tint = material_info.tint; let emissive_factor = material_info.emissive_factor; let metallic_factor = material_info.metallic_factor; @@ -672,58 +893,6 @@ impl Model { let occlusion_strength = material_info.occlusion_strength; let normal_scale = material_info.normal_scale; - let decode_rgba = |bytes: &Vec<u8>, label: &str| { - let load_start = Instant::now(); - let image = match image::load_from_memory(bytes) { - Ok(image) => image, - Err(err) => { - log::warn!( - "Failed to decode {} texture for material '{}': {} ({} bytes)", - label, - material_name, - err, - bytes.len() - ); - return None; - } - }; - log::trace!("Loading {} image to memory: {:?}", label, load_start.elapsed()); - - let rgba_start = Instant::now(); - let rgba = image.to_rgba8(); - log::trace!( - "Converting {} image to rgba8 took {:?}", - label, - rgba_start.elapsed() - ); - - let dimensions = image.dimensions(); - Some((rgba.into_raw(), dimensions)) - }; - - let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| { - decode_rgba(bytes, "diffuse") - }); - let processed_normal = normal_bytes.as_ref().and_then(|bytes| { - decode_rgba(bytes, "normal") - }); - let processed_emissive = emissive_bytes.as_ref().and_then(|bytes| { - decode_rgba(bytes, "emissive") - }); - let processed_metallic_roughness = - metallic_roughness_bytes.as_ref().and_then(|bytes| { - decode_rgba(bytes, "metallic_roughness") - }); - let processed_occlusion = occlusion_bytes.as_ref().and_then(|bytes| { - decode_rgba(bytes, "occlusion") - }); - - log::trace!( - "Parallel processing of material '{}' took: {:?}", - material_name, - material_start.elapsed() - ); - ProcessedMaterialTextures { name: material_name, diffuse: processed_diffuse, @@ -749,11 +918,6 @@ impl Model { }) .collect(); - log::trace!( - "Total parallel image processing took: {:?}", - parallel_start.elapsed() - ); - let mut materials = Vec::new(); let grey_texture = registry.grey_texture(graphics.clone()); @@ -761,7 +925,7 @@ impl Model { registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); for processed in processed_textures { - let start = Instant::now(); + puffin::profile_scope!("creating material"); let material_name = processed.name; let processed_diffuse = processed.diffuse; @@ -861,8 +1025,6 @@ impl Model { material.sync_uniform(&graphics); materials.push(material); - - log::trace!("Time to create GPU texture: {:?}", start.elapsed()); } let mut gpu_meshes = Vec::new(); @@ -890,15 +1052,6 @@ impl Model { }); } - let scale = import_scale.unwrap_or(1.0) as f32; - if (scale - 1.0).abs() > f32::EPSILON { - for vertex in &mut vertices { - vertex.position[0] *= scale; - vertex.position[1] *= scale; - vertex.position[2] *= scale; - } - } - let vertex_buffer = graphics .device @@ -935,6 +1088,9 @@ impl Model { path: ResourceReference::from_bytes(buffer.as_ref()), meshes: gpu_meshes, materials, + skins, + animations, + nodes, }; let handle = if let Some(label) = label { @@ -955,6 +1111,7 @@ pub trait DrawModel<'a> { material: &'a Material, camera_bind_group: &'a wgpu::BindGroup, light_bind_group: &'a wgpu::BindGroup, + skin_bind_group: Option<&'a wgpu::BindGroup>, ); fn draw_mesh_instanced( &mut self, @@ -963,6 +1120,7 @@ pub trait DrawModel<'a> { instances: Range<u32>, camera_bind_group: &'a wgpu::BindGroup, light_bind_group: &'a wgpu::BindGroup, + skin_bind_group: Option<&'a wgpu::BindGroup>, ); #[allow(unused)] @@ -991,8 +1149,9 @@ where material: &'b Material, camera_bind_group: &'b wgpu::BindGroup, light_bind_group: &'a wgpu::BindGroup, + skin_bind_group: Option<&'a wgpu::BindGroup>, ) { - self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group); + self.draw_mesh_instanced(mesh, material, 0..1, camera_bind_group, light_bind_group, skin_bind_group); } fn draw_mesh_instanced( @@ -1002,13 +1161,18 @@ where instances: Range<u32>, camera_bind_group: &'b wgpu::BindGroup, light_bind_group: &'a wgpu::BindGroup, + skin_bind_group: Option<&'a wgpu::BindGroup>, ) { self.set_vertex_buffer(0, mesh.vertex_buffer.slice(..)); self.set_index_buffer(mesh.index_buffer.slice(..), wgpu::IndexFormat::Uint32); self.set_bind_group(0, &material.bind_group, &[]); self.set_bind_group(1, camera_bind_group, &[]); self.set_bind_group(2, light_bind_group, &[]); - self.set_bind_group(3, &material.tint_bind_group, &[]); + + if let Some(skin_bg) = skin_bind_group { + self.set_bind_group(4, skin_bg, &[]); + } + self.draw_indexed(0..mesh.num_elements, 0, instances); } @@ -1036,6 +1200,7 @@ where instances.clone(), camera_bind_group, light_bind_group, + None, // Provide an AnimationComponent if available in a future update ); } } @@ -93,7 +93,7 @@ impl DropbearShaderPipeline for LightCubePipeline { let mut storage_buffer = None; let mut uniform_buffer = None; - if graphics.supports_storage { + if crate::graphics_features::is_enabled(crate::graphics_features::SupportsStorage) { storage_buffer = Some(StorageBuffer::new( &graphics.device, "light cube pipeline storage buffer", @@ -230,7 +230,7 @@ impl Vertex for VertexInput { } } -/// As mapped in `shaders/light.wgsl` as +/// As mapped in `shaders/light.slang` as /// ```wgsl /// struct InstanceInput { /// @location(5) model_matrix_0: vec4<f32>, @@ -1,5 +1,6 @@ use std::sync::Arc; use wgpu::{CompareFunction, DepthBiasState, StencilState}; +use crate::buffer::{StorageBuffer, UniformBuffer}; use crate::graphics::{InstanceRaw, SharedGraphicsContext}; use crate::model; use crate::model::Vertex; @@ -23,11 +24,11 @@ impl DropbearShaderPipeline for MainRenderPipeline { ); let bind_group_layouts = vec![ - &graphics.layouts.texture_bind_layout, // @group(0) + &graphics.layouts.material_bind_layout, // @group(0) &graphics.layouts.camera_bind_group_layout, // @group(1) &graphics.layouts.light_array_bind_group_layout, // @group(2) - &graphics.layouts.material_tint_bind_layout, // @group(3) - &graphics.layouts.shader_globals_bind_group_layout, // @group(4) + &graphics.layouts.shader_globals_bind_group_layout, // @group(3) + &graphics.layouts.skinning_bind_group_layout, // @group(4) ]; let pipeline_layout = @@ -52,7 +53,7 @@ impl DropbearShaderPipeline for MainRenderPipeline { }, fragment: Some(wgpu::FragmentState { module: &shader.module, - entry_point: if graphics.supports_storage { + entry_point: if crate::graphics_features::is_enabled(crate::graphics_features::SupportsStorage) { Some("s_fs_main") } else { Some("u_fs_main") @@ -5,6 +5,7 @@ impl ProcedurallyGeneratedObject { /// /// `size` is the full extents (width, height, depth). pub fn cuboid(size: glam::DVec3) -> Self { + puffin::profile_function!(); let half = (size / 2.0).as_vec3(); let uv_x = 1.0_f32; @@ -8,6 +8,7 @@ use crate::utils::ResourceReference; use crate::model::ModelVertex; use std::hash::{DefaultHasher, Hasher}; use std::sync::Arc; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use wgpu::util::DeviceExt; @@ -43,8 +44,9 @@ impl ProcedurallyGeneratedObject { graphics: Arc<SharedGraphicsContext>, material: Option<Material>, label: Option<&str>, - registry: &mut AssetRegistry, + registry: Arc<RwLock<AssetRegistry>>, ) -> Handle<Model> { + puffin::profile_function!(); let mut hasher = DefaultHasher::new(); hasher.write(bytemuck::cast_slice(&self.vertices)); hasher.write(bytemuck::cast_slice(&self.indices)); @@ -54,7 +56,9 @@ impl ProcedurallyGeneratedObject { .map(|s| s.to_string()) .unwrap_or_else(|| format!("procedural_{hash:016x}")); - if let Some(handle) = registry.model_handle_by_hash(hash) { + let mut _rguard = registry.write(); + + if let Some(handle) = _rguard.model_handle_by_hash(hash) { return handle; } @@ -87,14 +91,14 @@ impl ProcedurallyGeneratedObject { }; let material = material.unwrap_or_else(|| { - let grey_handle = registry.grey_texture(graphics.clone()); + let grey_handle = _rguard.grey_texture(graphics.clone()); let flat_normal_handle = - registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); - let grey = registry + _rguard.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); + let grey = _rguard .get_texture(grey_handle) .expect("Grey texture handle missing") .clone(); - let flat_normal = registry + let flat_normal = _rguard .get_texture(flat_normal_handle) .expect("Flat normal texture handle missing") .clone(); @@ -114,8 +118,11 @@ impl ProcedurallyGeneratedObject { path: ResourceReference::from_bytes(hash.to_le_bytes()), meshes: vec![mesh], materials: vec![material], + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), }; - registry.add_model_with_label(label, model) + _rguard.add_model_with_label(label, model) } } @@ -93,17 +93,20 @@ impl Manager { graphics: Arc<SharedGraphicsContext>, event_loop: &ActiveEventLoop, ) -> Vec<SceneCommand> { + puffin::profile_function!(); // transition scene if let Some(next_scene_name) = self.next_scene.take() { if let Some(current_scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(current_scene_name) { { + puffin::profile_scope!("exit old scene", current_scene_name); scene.write().exit(event_loop); } } if let Some(scene) = self.scenes.get_mut(&next_scene_name) { { + puffin::profile_scope!("load new scene", &next_scene_name); scene.write().load(graphics.clone()); } } @@ -115,6 +118,7 @@ impl Manager { && let Some(scene) = self.scenes.get_mut(scene_name) { { + puffin::profile_scope!("update new scene", &scene_name); scene.write().update(dt, graphics.clone()); } let command = scene.write().run_command(); @@ -124,6 +128,7 @@ impl Manager { if current == &target { // reload the scene if let Some(scene) = self.scenes.get_mut(current) { + puffin::profile_scope!("reload the scene", ¤t); scene.write().exit(event_loop); scene.write().load(graphics.clone()); @@ -155,25 +160,31 @@ impl Manager { dt: f32, graphics: Arc<SharedGraphicsContext>, ) { + puffin::profile_function!(); if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) { + puffin::profile_scope!("physics-update the scene", &scene_name); scene.write().physics_update(dt, graphics.clone()) } } pub fn render<'a>(&mut self, graphics: Arc<SharedGraphicsContext>) { + puffin::profile_function!(); if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) { + puffin::profile_scope!("render the scene", &scene_name); scene.write().render(graphics.clone()) } } pub fn handle_event(&mut self, event: &WindowEvent) { + puffin::profile_function!(); if let Some(scene_name) = &self.current_scene && let Some(scene) = self.scenes.get_mut(scene_name) { + puffin::profile_scope!("handle winit window event in the scene", &scene_name); scene.write().handle_event(event); } } @@ -39,6 +39,7 @@ impl Shader { shader_file_contents: &str, label: Option<&str>, ) -> Self { + puffin::profile_function!(); let module = graphics .device .create_shader_module(wgpu::ShaderModuleDescriptor { @@ -61,6 +62,7 @@ impl Shader { } pub fn from_slang(graphics: Arc<SharedGraphicsContext>, shader: &CompiledSlangShader) -> Self { + puffin::profile_function!(); let module = graphics .device .create_shader_module(shader.create_wgpu_shader()); @@ -44,44 +44,42 @@ var s_diffuse: sampler; var t_normal: texture_2d<f32>; @group(0) @binding(3) var s_normal: sampler; +@group(0) @binding(4) +var<uniform> u_material: MaterialUniform; @group(1) @binding(0) var<uniform> u_camera: CameraUniform; @group(2) @binding(0) var<storage, read> s_light_array: array<Light>; -@group(2) @binding(0) -var<uniform> u_light_array: array<Light, 10>; // when storage is not available @group(3) @binding(0) -var<uniform> u_material: MaterialUniform; +var<uniform> u_globals: Globals; @group(4) @binding(0) -var<uniform> u_globals: Globals; +var<storage, read> s_skinning: array<mat4x4<f32>>; struct InstanceInput { - @location(5) model_matrix_0: vec4<f32>, - @location(6) model_matrix_1: vec4<f32>, - @location(7) model_matrix_2: vec4<f32>, - @location(8) model_matrix_3: vec4<f32>, - - @location(9) normal_matrix_0: vec3<f32>, - @location(10) normal_matrix_1: vec3<f32>, - @location(11) normal_matrix_2: vec3<f32>, + @location(8) model_matrix_0: vec4<f32>, + @location(9) model_matrix_1: vec4<f32>, + @location(10) model_matrix_2: vec4<f32>, + @location(11) model_matrix_3: vec4<f32>, + + @location(12) normal_matrix_0: vec3<f32>, + @location(13) normal_matrix_1: vec3<f32>, + @location(14) normal_matrix_2: vec3<f32>, }; struct VertexInput { @location(0) position: vec3<f32>, - @location(2) normal: vec3<f32>, - @location(3) tangent: vec3<f32>, - @location(1) tex_coords0: vec2<f32>, - @location(1) tex_coords1: vec2<f32>, - @location(1) colour0: vec4<f32>, - @location(1) joints(0): vec4<u32>, - @location(1) joints(0): vec4<u32>, - - - @location(4) bitangent: vec3<f32>, + @location(1) normal: vec3<f32>, + @location(2) tangent: vec4<f32>, + @location(3) tex_coords0: vec2<f32>, + @location(4) tex_coords1: vec2<f32>, + @location(5) colour0: vec4<f32>, + + @location(6) joints: vec4<u32>, + @location(7) weights: vec4<f32>, }; struct VertexOutput { @@ -110,14 +108,36 @@ fn vs_main( instance.normal_matrix_2, ); - let world_normal = normalize(normal_matrix * model.normal); - let world_tangent = normalize(normal_matrix * model.tangent); - let world_bitangent = normalize(normal_matrix * model.bitangent); - let world_position = model_matrix * vec4<f32>(model.position, 1.0); + var skin_matrix = mat4x4<f32>( + vec4<f32>(1.0, 0.0, 0.0, 0.0), + vec4<f32>(0.0, 1.0, 0.0, 0.0), + vec4<f32>(0.0, 0.0, 1.0, 0.0), + vec4<f32>(0.0, 0.0, 0.0, 1.0) + ); + + if (dot(model.weights, vec4<f32>(1.0)) > 0.0) { + let j = model.joints; + let w = model.weights; + + skin_matrix = + (s_skinning[j.x] * w.x) + + (s_skinning[j.y] * w.y) + + (s_skinning[j.z] * w.z) + + (s_skinning[j.w] * w.w); + } + + let world_position = model_matrix * skin_matrix * vec4<f32>(model.position, 1.0); + + let skin_normal = (skin_matrix * vec4<f32>(model.normal, 0.0)).xyz; + let skin_tangent = (skin_matrix * vec4<f32>(model.tangent.xyz, 0.0)).xyz; + + let world_normal = normalize(normal_matrix * skin_normal); + let world_tangent = normalize(normal_matrix * skin_tangent); + let world_bitangent = normalize(cross(world_normal, world_tangent) * model.tangent.w); var out: VertexOutput; out.clip_position = u_camera.view_proj * world_position; - out.tex_coords = model.tex_coords; + out.tex_coords = model.tex_coords0; out.world_normal = world_normal; out.world_position = world_position.xyz; out.world_tangent = world_tangent; @@ -213,29 +233,24 @@ fn apply_normal_map( return normalize(tbn * normal_ts); } -// when storage is supported @fragment fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { let uv = in.tex_coords * u_material.uv_tiling; var tex_color = textureSample(t_diffuse, s_diffuse, uv); var object_normal = textureSample(t_normal, s_normal, uv); - let base_colour = tex_color * u_material.colour; + let base_colour = tex_color * u_material.base_colour; - if (base_colour.a < 0.1) { + if (base_colour.a < u_material.alpha_cutoff) { discard; } - let normal = normalize(world_normal); - let tangent = normalize(world_tangent.xyz); - let bitangent = cross(n, t) * world_tangent.w; - let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); let world_normal = apply_normal_map( in.world_normal, in.world_tangent, - bitangent, + in.world_bitangent, object_normal.xyz, ); @@ -259,48 +274,3 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { return vec4<f32>(final_color, base_colour.a); } -// when storage is NOT supported -@fragment -fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { - let uv = in.tex_coords * u_material.uv_tiling; - var tex_color = textureSample(t_diffuse, s_diffuse, uv); - var object_normal = textureSample(t_normal, s_normal, uv); - - let base_colour = tex_color * u_material.colour; - - if (base_colour.a < 0.1) { - discard; - } - - let normal = normalize(world_normal); - let tangent = normalize(world_tangent.xyz); - let bitangent = cross(n, t) * world_tangent.w; - - let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); - - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - bitangent, - object_normal.xyz, - ); - - let ambient = vec3<f32>(1.0) * u_globals.ambient_strength * base_colour.xyz; - var final_color = ambient; - - for(var i = 0u; i < min(u_globals.num_lights, c_max_lights); i += 1u) { - let light = u_light_array[i]; - - let light_type = i32(light.color.w + 0.1); - - if (light_type == 0) { - final_color += directional_light(light, world_normal, view_dir, base_colour.xyz, in.world_position); - } else if (light_type == 1) { - final_color += point_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } else if (light_type == 2) { - final_color += spot_light(light, in.world_position, world_normal, view_dir, base_colour.xyz); - } - } - - return vec4<f32>(final_color, base_colour.a); -} @@ -24,6 +24,7 @@ impl CubeTexture { mag_filter: wgpu::FilterMode, label: Option<&str>, ) -> Self { + puffin::profile_function!(); let texture = device.create_texture(&wgpu::TextureDescriptor { label, size: wgpu::Extent3d { @@ -81,6 +82,7 @@ pub struct HdrLoader { impl HdrLoader { pub fn new(device: &wgpu::Device) -> Self { + puffin::profile_function!(); let module = device.create_shader_module(wgpu::include_wgsl!("shaders/equirectangular.wgsl")); let texture_format = wgpu::TextureFormat::Rgba32Float; let equirect_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor { @@ -139,6 +141,7 @@ impl HdrLoader { dst_size: u32, label: Option<&str>, ) -> anyhow::Result<CubeTexture> { + puffin::profile_function!(); let loader = Self::new(device); let hdr_decoder = HdrDecoder::new(Cursor::new(data))?; @@ -255,6 +258,7 @@ pub struct SkyPipeline { impl SkyPipeline { pub fn new(graphics: Arc<SharedGraphicsContext>, sky_texture: CubeTexture) -> Self { + puffin::profile_function!(); let environment_bind_group = graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { label: Some("environment_bind_group"), layout: &graphics.layouts.environment_bind_group_layout, @@ -85,6 +85,7 @@ impl Texture { mag_filter: wgpu::FilterMode, label: Option<&str>, ) -> Self { + puffin::profile_function!(label.unwrap_or("create 2d texture")); let size = wgpu::Extent3d { width, height, @@ -110,6 +111,7 @@ impl Texture { dimension: wgpu::TextureDimension, mag_filter: wgpu::FilterMode, ) -> Self { + puffin::profile_function!(label.unwrap_or("create texture")); let texture = device.create_texture(&wgpu::TextureDescriptor { label, size, @@ -148,6 +150,7 @@ impl Texture { device: &wgpu::Device, label: Option<&str>, ) -> Self { + puffin::profile_function!(label.unwrap_or("depth texture")); let size = wgpu::Extent3d { width: config.width.max(1), height: config.height.max(1), @@ -198,6 +201,7 @@ impl Texture { device: &wgpu::Device, label: Option<&str>, ) -> Self { + puffin::profile_function!(label.unwrap_or("viewport texture")); let size = wgpu::Extent3d { width: config.width.max(1), height: config.height.max(1), @@ -235,6 +239,7 @@ impl Texture { path: &PathBuf, label: Option<&str>, ) -> anyhow::Result<Self> { + puffin::profile_function!(label.unwrap_or("")); let data = fs::read(path)?; Ok(Self::from_bytes(graphics.clone(), &data, label)) } @@ -243,6 +248,7 @@ impl Texture { /// /// If you want more customisability in the texture being generated, you can use [Self::from_bytes_verbose] pub fn from_bytes(graphics: Arc<SharedGraphicsContext>, bytes: &[u8], label: Option<&str>) -> Self { + puffin::profile_function!(label.unwrap_or("")); Self::from_bytes_verbose_mipmapped(graphics, bytes, None, None, None, label) } @@ -257,6 +263,7 @@ impl Texture { sampler: Option<wgpu::SamplerDescriptor>, label: Option<&str>, ) -> Self { + puffin::profile_function!(label.unwrap_or("")); let texture = Self::from_bytes_verbose( graphics.clone(), bytes, @@ -289,23 +296,26 @@ impl Texture { sampler: Option<wgpu::SamplerDescriptor>, label: Option<&str>, ) -> Self { + puffin::profile_function!(label.unwrap_or("")); let hash = AssetRegistry::hash_bytes(bytes); - let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) { - Ok(image) => { - let rgba = image.to_rgba8().into_raw(); - let dims = dimensions.unwrap_or_else(|| image.dimensions()); - (rgba, dims) - } - Err(err) => { - if let Some(dims) = dimensions { - let expected_len = (dims.0 as usize) - .saturating_mul(dims.1 as usize) - .saturating_mul(4); - if bytes.len() == expected_len { - (bytes.to_vec(), dims) - } else { - log::error!( + let (diffuse_rgba, dimensions) = { + puffin::profile_scope!("load from memory image"); + match image::load_from_memory(bytes) { + Ok(image) => { + let rgba = image.to_rgba8().into_raw(); + let dims = dimensions.unwrap_or_else(|| image.dimensions()); + (rgba, dims) + } + Err(err) => { + if let Some(dims) = dimensions { + let expected_len = (dims.0 as usize) + .saturating_mul(dims.1 as usize) + .saturating_mul(4); + if bytes.len() == expected_len { + (bytes.to_vec(), dims) + } else { + log::error!( "Texture [{:?}] decode failed ({:?}); expected {} bytes for raw RGBA ({}x{}), got {}. Falling back.", label, err, @@ -314,15 +324,16 @@ impl Texture { dims.1, bytes.len() ); - (vec![255, 0, 255, 255], (1, 1)) - } - } else { - log::error!( + (vec![255, 0, 255, 255], (1, 1)) + } + } else { + log::error!( "Texture [{:?}] decode failed ({:?}) and no dimensions were provided; falling back to 1x1 magenta.", label, err ); - (vec![255, 0, 255, 255], (1, 1)) + (vec![255, 0, 255, 255], (1, 1)) + } } } }; @@ -355,6 +366,7 @@ impl Texture { debug_assert!(diffuse_rgba.len() >= (unpadded_bytes_per_row * size.height) as usize); if padded_bytes_per_row == unpadded_bytes_per_row { + puffin::profile_scope!("write to texture"); graphics.queue.write_texture( wgpu::TexelCopyTextureInfo { texture: &texture, @@ -371,6 +383,7 @@ impl Texture { size, ); } else { + puffin::profile_scope!("write to texture"); let mut padded = vec![0u8; (padded_bytes_per_row * size.height) as usize]; let src_stride = unpadded_bytes_per_row as usize; let dst_stride = padded_bytes_per_row as usize; @@ -472,6 +485,7 @@ impl DropbearEngineLogo { /// /// Returns (the bytes, width, height) in resp order. pub fn generate() -> anyhow::Result<(Vec<u8>, u32, u32)> { + puffin::profile_function!("generate dropbear engine logo"); let image = image::load_from_memory(Self::DROPBEAR_ENGINE_LOGO)?.into_rgba8(); let (width, height) = image.dimensions(); let rgba = image.into_raw(); @@ -152,12 +152,14 @@ impl ResourceReference { /// Creates a new `ResourceReference` from bytes pub fn from_bytes(bytes: impl AsRef<[u8]>) -> Self { + puffin::profile_function!(); Self { ref_type: ResourceReferenceType::Bytes(bytes.as_ref().to_vec()), } } pub fn from_reference(ref_type: ResourceReferenceType) -> Self { + puffin::profile_function!(format!("{:?}", ref_type)); match ref_type { ResourceReferenceType::File(reference) => { let canonical = canonicalize_euca_uri(&reference) @@ -172,6 +174,7 @@ impl ResourceReference { /// Creates a [`ResourceReference`] directly from an euca URI (e.g. `euca://models/cube.glb`). pub fn from_euca_uri(uri: impl AsRef<str>) -> anyhow::Result<Self> { + puffin::profile_function!(uri.as_ref()); let canonical = canonicalize_euca_uri(uri.as_ref())?; Ok(Self { ref_type: ResourceReferenceType::File(canonical), @@ -212,6 +215,7 @@ impl ResourceReference { /// /// Returns `None` if the path doesn't contain "resources" or if the path after resources is empty. pub fn from_path(full_path: impl AsRef<Path>) -> anyhow::Result<Self> { + puffin::profile_function!(full_path.as_ref().display().to_string()); let path = full_path.as_ref(); let components: Vec<_> = path.components().collect(); @@ -29,6 +29,7 @@ pub use dropbear_traits as traits; pub use egui; pub use rapier3d; +use dropbear_engine::animation::AnimationComponent; use dropbear_engine::camera::Camera; use dropbear_engine::entity::{EntityTransform, MeshRenderer}; use dropbear_traits::registry::ComponentRegistry; @@ -69,6 +70,7 @@ pub fn register_components( component_registry.register_with_default::<ColliderGroup>(); component_registry.register_with_default::<KCC>(); component_registry.register_with_default::<UIComponent>(); + component_registry.register_with_default::<AnimationComponent>(); component_registry.register_converter::<MeshRenderer, SerializedMeshRenderer, _>( |_, _, renderer| { @@ -157,10 +157,13 @@ impl SceneConfig { let model = std::sync::Arc::new(Model { label: "None".to_string(), + hash: *id, path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }), meshes: Vec::new(), materials: Vec::new(), - id: ModelId(*id), + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), }); let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model); MeshRenderer::from_handle_with_import_scale(loaded, import_scale) @@ -571,7 +571,7 @@ impl Scene for Editor { }); render_pass.set_pipeline(pipeline.pipeline()); render_pass.set_vertex_buffer(1, instance_buffer.slice(..)); - render_pass.set_bind_group(4, globals_bind_group, &[]); + render_pass.set_bind_group(3, globals_bind_group, &[]); render_pass.draw_model_instanced( &model, 0..instance_count, @@ -552,10 +552,13 @@ impl SignalController for Editor { let model = std::sync::Arc::new(Model { label: "None".to_string(), + hash: unassigned_id, path: reference, meshes: Vec::new(), materials: Vec::new(), - id: ModelId(unassigned_id), + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), }); let loaded_model = LoadedModel::new_raw( @@ -625,10 +628,13 @@ impl SignalController for Editor { let model = std::sync::Arc::new(Model { label: "None".to_string(), + hash: unassigned_id, path: reference, meshes: Vec::new(), materials: Vec::new(), - id: ModelId(unassigned_id), + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), }); let loaded_model = LoadedModel::new_raw(&dropbear_engine::asset::ASSET_REGISTRY, model); @@ -280,10 +280,13 @@ async fn load_renderer_from_serialized( ResourceReferenceType::Unassigned { id } => { let model = std::sync::Arc::new(Model { label: "None".to_string(), + hash: *id, path: ResourceReference::from_reference(ResourceReferenceType::Unassigned { id: *id }), meshes: Vec::new(), materials: Vec::new(), - id: ModelId(*id), + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), }); let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model); @@ -0,0 +1,55 @@ +package com.dropbear.animation + +import com.dropbear.EntityId +import com.dropbear.ecs.Component + +class AnimationComponent(parentEntity: EntityId) : Component(parentEntity, "AnimationComponent") { + var activeAnimationIndex: Int? + get() = getActiveAnimationIndex() + set(value) = setActiveAnimationIndex(value) + + var time: Float + get() = getTime() + set(value) = setTime(value) + + var speed: Float + get() = getSpeed() + set(value) = setSpeed(value) + + var looping: Boolean + get() = getLooping() + set(value) = setLooping(value) + + var isPlaying: Boolean + get() = getIsPlaying() + set(value) = setIsPlaying(value) + + fun pause() { + isPlaying = false + } + + fun play() { + isPlaying = true + } + + fun stop() { + isPlaying = false + time = 0f + activeAnimationIndex = null + } + + fun reset() { + time = 0f + } +} + +expect fun AnimationComponent.getActiveAnimationIndex(): Int? +expect fun AnimationComponent.setActiveAnimationIndex(index: Int?) +expect fun AnimationComponent.getTime(): Float +expect fun AnimationComponent.setTime(value: Float) +expect fun AnimationComponent.getSpeed(): Float +expect fun AnimationComponent.setSpeed(value: Float) +expect fun AnimationComponent.getLooping(): Boolean +expect fun AnimationComponent.setLooping(value: Boolean) +expect fun AnimationComponent.getIsPlaying(): Boolean +expect fun AnimationComponent.setIsPlaying(value: Boolean) @@ -1,43 +0,0 @@ -package com.dropbear.asset - -import com.dropbear.DropbearEngine - -/** - * A handle that describes the type of asset in the ASSET_REGISTRY - */ -class AssetHandle(private val id: Long): Handle(id) { - /** - * Converts an [AssetHandle] to a [ModelHandle]. - * - * It can return null if the asset is not a model. - */ - fun asModelHandle(): ModelHandle? { - val result = isModelHandle(id) - return if (result) { - ModelHandle(id) - } else { - null - } - } - - override fun asAssetHandle(): AssetHandle { - return this - } - - /** - * Converts an [AssetHandle] to a [TextureHandle]. - * - * It can return null if the asset is not a texture. - */ - fun asTextureHandle(): TextureHandle? { - return if (isTextureHandle(id)) TextureHandle(id) else null - } - - override fun toString(): String { - return "AssetHandle(id=$id)" - } -} - - -internal expect fun isTextureHandle(id: Long): Boolean -internal expect fun isModelHandle(id: Long): Boolean @@ -0,0 +1,4 @@ +package com.dropbear.asset + +interface AssetType { +} @@ -1,46 +1,28 @@ package com.dropbear.asset +import com.dropbear.utils.ID + /** * Describes a handle of an asset, or anything really. * - * Aims to allow people to group up different handle types ([AssetHandle], [ModelHandle] etc...) + * Aims to allow people to group up different handle types * into a list or a vector. * * All handles must be positive, non-zero values. If the id does not follow that rule, it is considered invalid. */ -abstract class Handle(private val id: Long) { +class Handle<T: AssetType>(private val id: Long): ID(id) { init { - require(id > 0) { "Handle id must be a positive, non-zero value. Got: $id" } + require(id > 0) { "Handle id must be a positive value. Got: $id" } } - /** - * Returns the raw id of the handle - */ - fun raw(): Long = id - - /** - * Returns the handle as an [AssetHandle]. - * - * This will not return null as all handles are a type of [AssetHandle]. - */ - abstract fun asAssetHandle(): AssetHandle - - override fun toString(): String { - return "Handle(id=$id)" + companion object { + fun <T: AssetType> invalid() = Handle<T>(0) } - override fun equals(other: Any?): Boolean { - if (this === other) return true - - val otherAsset = when (other) { - is Handle -> other.asAssetHandle() - is AssetHandle -> other - else -> return false - } - - val thisAsset = asAssetHandle() - return thisAsset.raw() == otherAsset.raw() + fun raw(): Long { + return id } +} - override fun hashCode(): Int = asAssetHandle().raw().hashCode() -} +typealias TextureHandle = Handle<Texture> +typealias ModelHandle = Handle<Model> @@ -0,0 +1,34 @@ +package com.dropbear.asset + +import com.dropbear.asset.model.Mesh +import com.dropbear.asset.model.Material +import com.dropbear.asset.model.Skin +import com.dropbear.asset.model.Animation +import com.dropbear.asset.model.Node + +class Model(val id: Long): AssetType { + val label: String + get() = getLabel() + + val meshes: List<Mesh> + get() = getMeshes() + + val materials: List<Material> + get() = getMaterials() + + val skins: List<Skin> + get() = getSkins() + + val animations: List<Animation> + get() = getAnimations() + + val nodes: List<Node> + get() = getNodes() +} + +expect fun Model.getLabel(): String +expect fun Model.getMeshes(): List<Mesh> +expect fun Model.getMaterials(): List<Material> +expect fun Model.getSkins(): List<Skin> +expect fun Model.getAnimations(): List<Animation> +expect fun Model.getNodes(): List<Node> @@ -1,13 +0,0 @@ -package com.dropbear.asset - -/** - * Another type of asset handle, this wraps the id of the asset - * into something that only models can access. - */ -class ModelHandle(private val id: Long): Handle(id) { - override fun asAssetHandle(): AssetHandle = AssetHandle(id) - - override fun toString(): String { - return "ModelHandle(id=$id)" - } -} @@ -0,0 +1,22 @@ +package com.dropbear.asset + +class Texture(val id: Long): AssetType { + var label: String? + get() = getLabel() + set(value) = setLabel(value) + + val width: Int + get() = getWidth() + + val height: Int + get() = getHeight() + + val depth: Int + get() = getDepth() +} + +expect fun Texture.getLabel(): String? +expect fun Texture.setLabel(value: String?) +expect fun Texture.getWidth(): Int +expect fun Texture.getHeight(): Int +expect fun Texture.getDepth(): Int @@ -1,21 +0,0 @@ -package com.dropbear.asset - -/** - * Another type of asset handle, this wraps the id into - * another form that only texture related functions can use. - */ -class TextureHandle(private val id: Long): Handle(id) { - /** - * The name of the texture/material. - */ - val name: String? - get() = getTextureName(id) - - override fun asAssetHandle(): AssetHandle = AssetHandle(id) - - override fun toString(): String { - return "TextureHandle(id=$id)" - } -} - -internal expect fun TextureHandle.getTextureName(id: Long): String? @@ -0,0 +1,8 @@ +package com.dropbear.asset.model + +data class Mesh( + val name: String, + val numElements: Int, + val materialIndex: Int, + val vertices: List<ModelVertex> +) @@ -0,0 +1,79 @@ +package com.dropbear.math + +import kotlin.math.* + +/** + * Represents an angle with support for both degrees and radians. + * Provides normalization and common trigonometric operations. + */ +class Angle private constructor(private val radians: Double) { + + companion object { + /** Creates an Angle from degrees */ + fun fromDegrees(degrees: Double): Angle = Angle(degreesToRadians(degrees)) + + /** Creates an Angle from radians */ + fun fromRadians(radians: Double): Angle = Angle(radians) + + /** Zero angle */ + val ZERO = Angle(0.0) + + /** Right angle (90 degrees) */ + val RIGHT = fromDegrees(90.0) + + /** Straight angle (180 degrees) */ + val STRAIGHT = fromDegrees(180.0) + + /** Full rotation (360 degrees) */ + val FULL = fromDegrees(360.0) + } + + /** Get the angle value in degrees */ + val degrees: Double + get() = radiansToDegrees(radians) + + /** Get the angle value in radians */ + fun toRadians(): Double = radians + + /** Get the angle value in degrees */ + fun toDegrees(): Double = degrees + + /** Normalize the angle to range [0, 360) degrees or [0, 2π) radians */ + fun normalized(): Angle { + val normalized = radians % (2 * PI) + return Angle(if (normalized < 0) normalized + 2 * PI else normalized) + } + + /** Normalize the angle to range [-180, 180) degrees or [-π, π) radians */ + fun normalizedSigned(): Angle { + val norm = normalized().radians + return if (norm > PI) Angle(norm - 2 * PI) else Angle(norm) + } + + operator fun plus(other: Angle): Angle = Angle(radians + other.radians) + operator fun minus(other: Angle): Angle = Angle(radians - other.radians) + operator fun times(scalar: Double): Angle = Angle(radians * scalar) + operator fun div(scalar: Double): Angle = Angle(radians / scalar) + operator fun unaryMinus(): Angle = Angle(-radians) + + operator fun compareTo(other: Angle): Int = radians.compareTo(other.radians) + + fun sin(): Double = sin(radians) + fun cos(): Double = cos(radians) + fun tan(): Double = tan(radians) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Angle) return false + return radians == other.radians + } + + override fun hashCode(): Int = radians.hashCode() + + override fun toString(): String = "${degrees}°" +} + +fun Double.degrees(): Angle = Angle.fromDegrees(this) +fun Double.radians(): Angle = Angle.fromRadians(this) +fun Int.degrees(): Angle = Angle.fromDegrees(this.toDouble()) +fun Int.radians(): Angle = Angle.fromRadians(this.toDouble()) @@ -16,4 +16,10 @@ inline fun buildUI(block: UIBuilder.() -> Unit): UIInstructionSet { fun UIBuilder.add(instruction: UIInstruction) { instructions.add(instruction) -} +} + +fun UIBuilder.add(instructionSet: UIInstructionSet) { + instructions.addAll(instructionSet) +} + +fun UIBuilder.add(block: UIBuilder.() -> Widget) = add(block().toInstruction()) @@ -1,48 +0,0 @@ -package com.dropbear.ui.widgets - -import com.dropbear.ui.UIBuilder -import com.dropbear.ui.UIInstruction -import com.dropbear.ui.Widget -import com.dropbear.ui.WidgetId -import com.dropbear.ui.styling.Alignment - -class Align( - override var id: WidgetId, - var align: Alignment, - var widgets: MutableList<Widget> = mutableListOf(), -): Widget() { - companion object { - fun center(id: WidgetId) = Align(align = Alignment.CENTER, id = id) - } - - sealed class AlignmentInstruction: UIInstruction { - data class StartAlignmentBlock(val id: WidgetId, val align: Align) : AlignmentInstruction() - data class EndAlignmentBlock(val id: WidgetId) : AlignmentInstruction() - } - - override fun toInstruction(): List<UIInstruction> { - val instructions = mutableListOf<UIInstruction>() - instructions.add(AlignmentInstruction.StartAlignmentBlock(id, this)) - widgets.forEach { widget -> - widget.toInstruction().forEach { instruction -> - instructions.add(instruction) - } - } - instructions.add(AlignmentInstruction.EndAlignmentBlock(id)) - return instructions.toList() - } -} - -fun UIBuilder.center(id: WidgetId, block: UIBuilder.() -> Unit = {}) { - val align = Align.center(id) - instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align)) - block(this) - instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id)) -} - -fun UIBuilder.align(alignment: Alignment, id: WidgetId, block: UIBuilder.() -> Unit = {}) { - val align = Align(align = alignment, id = id) - instructions.add(Align.AlignmentInstruction.StartAlignmentBlock(align.id, align)) - block(this) - instructions.add(Align.AlignmentInstruction.EndAlignmentBlock(align.id)) -} @@ -1,114 +0,0 @@ -package com.dropbear.ui.widgets - -import com.dropbear.ui.UIBuilder -import com.dropbear.ui.UIInstruction -import com.dropbear.ui.Widget -import com.dropbear.ui.WidgetId -import com.dropbear.ui.styling.Alignment -import com.dropbear.ui.styling.Border -import com.dropbear.ui.styling.BorderRadius -import com.dropbear.ui.styling.DynamicButtonStyle -import com.dropbear.ui.styling.Padding -import com.dropbear.ui.styling.TextStyle -import com.dropbear.ui.styling.fonts.TextAlignment -import com.dropbear.utils.Colour -import com.dropbear.utils.ID - -class Button( - var text: String, - var alignment: Alignment, - var padding: Padding, - var borderRadius: BorderRadius, - var style: DynamicButtonStyle, - var hoverStyle: DynamicButtonStyle, - var downStyle: DynamicButtonStyle, - id: WidgetId = WidgetId(text.hashCode().toLong()), -): Widget() { - override var id: WidgetId - - init { - this.id = id - } - - val clicked: Boolean - get() = getClicked() - - val hovering: Boolean - get() = getHovering() - - companion object { - fun styled(text: String, id: String = text) : Button { - val style = DynamicButtonStyle( - fill = Colour.BACKGROUND_3, - text = TextStyle( - colour = Colour.WHITE.adjust(0.8), - align = TextAlignment.Center - ), - border = Border( - colour = Colour.BACKGROUND_1, - width = 1.0 - ) - ) - - val hoverStyle = DynamicButtonStyle( - fill = Colour.BACKGROUND_3.adjust(1.2), - border = Border(Colour.WHITE.adjust(0.75), 1.0), - ) - - val downStyle = DynamicButtonStyle( - fill = Colour.BACKGROUND_3.adjust(0.8), - border = Border(Colour.WHITE, 1.0) - ) - - val result = Button( - text = text, - alignment = Alignment.CENTER, - padding = Padding.balanced(20.0, 10.0), - borderRadius = BorderRadius.uniform(6.0), - style = style, - hoverStyle = hoverStyle, - downStyle = downStyle, - ) - - result.id = WidgetId(id.hashCode().toLong()) - - return result - } - - fun unstyled(text: String, id: String = text) : Button { - val result = Button( - text = text, - alignment = Alignment.CENTER, - padding = Padding.zero(), - borderRadius = BorderRadius(), - style = DynamicButtonStyle(), - hoverStyle = DynamicButtonStyle(), - downStyle = DynamicButtonStyle(), - ) - - result.id = WidgetId(id.hashCode().toLong()) - - return result - } - } - - sealed class ButtonInstruction: UIInstruction { - data class Button(val id: WidgetId, val button: com.dropbear.ui.widgets.Button) : ButtonInstruction() - } - - override fun toInstruction(): List<UIInstruction> { - return listOf(ButtonInstruction.Button(this.id, this)) - } -} - -expect fun Button.getClicked(): Boolean -expect fun Button.getHovering(): Boolean - -// fits that of yakui_widgets::shorthand::button -fun UIBuilder.button(text: String, block: Button.() -> Unit = {}): Button { - val btn = Button.styled(text).apply(block) - btn.toInstruction().forEach { - instructions.add(it) - } - return btn -} @@ -1,48 +0,0 @@ -package com.dropbear.ui.widgets - -import com.dropbear.ui.UIBuilder -import com.dropbear.ui.UIInstruction -import com.dropbear.ui.Widget -import com.dropbear.ui.WidgetId - -class Checkbox( - override val id: WidgetId, - initiallyChecked: Boolean, -) : Widget() { - private var cachedChecked: Boolean = initiallyChecked - - /** - * To set a new checkbox value, make it so the class is different next frame. - */ - var checked: Boolean - get() { - if (hasCheckedState()) { - cachedChecked = getChecked() - } - return cachedChecked - } - private set(value) { - cachedChecked = value - } - - init { - checked = initiallyChecked - } - - sealed class CheckboxInstruction: UIInstruction { - data class Checkbox(val id: WidgetId, val checked: Boolean) : CheckboxInstruction() - } - - override fun toInstruction(): List<UIInstruction> { - return listOf(CheckboxInstruction.Checkbox(id, checked)) - } -} - -expect fun Checkbox.getChecked(): Boolean -expect fun Checkbox.hasCheckedState(): Boolean - -fun UIBuilder.checkbox(checked: Boolean, id: WidgetId, block: Checkbox.() -> Unit = {}): Checkbox { - val cb = Checkbox(id, checked).apply(block) - cb.toInstruction().forEach { instruction -> instructions.add(instruction) } - return cb -} @@ -1,69 +0,0 @@ -package com.dropbear.ui.widgets - -import com.dropbear.math.Vector2d -import com.dropbear.ui.UIBuilder -import com.dropbear.ui.UIInstruction -import com.dropbear.ui.Widget -import com.dropbear.ui.WidgetId -import com.dropbear.ui.buildUI -import com.dropbear.utils.Colour - -class ColouredBox( - override val id: WidgetId, - var colour: Colour = Colour.WHITE, - var minSize: Vector2d = Vector2d.zero(), -) : Widget() { - var widgets: MutableList<UIInstruction> = mutableListOf() - - companion object { - fun sized(id: WidgetId, colour: Colour, size: Vector2d): ColouredBox { - return ColouredBox(id, colour, size) - } - - fun container(id: WidgetId, colour: Colour): ColouredBox { - return ColouredBox(id, colour, Vector2d.zero()) - } - } - - sealed class ColouredBoxInstruction: UIInstruction { - data class StartColouredBoxInstruction(val id: WidgetId, val box: ColouredBox): ColouredBoxInstruction() - data class EndColouredBoxInstruction(val id: WidgetId): ColouredBoxInstruction() - } - - override fun toInstruction(): List<UIInstruction> { - val list = mutableListOf<UIInstruction>() - list.add(ColouredBoxInstruction.StartColouredBoxInstruction(this.id, this)) - list.addAll(widgets) - list.add(ColouredBoxInstruction.EndColouredBoxInstruction(this.id)) - return list.toList() - } - - fun addChild(widget: Widget) { - widgets.addAll(widget.toInstruction()) - } -} - -fun UIBuilder.colouredBox(id: WidgetId, colour: Colour, minSize: Vector2d, block: ColouredBox.() -> Unit = {}): ColouredBox { - val box = ColouredBox(id, colour, minSize).apply(block) - instructions.addAll(box.toInstruction()) - return box -} - -fun UIBuilder.colouredBoxContainer(id: WidgetId, colour: Colour, children: UIBuilder.() -> Unit): ColouredBox { - val box = ColouredBox(id = id, colour = colour) - val newUI = UIBuilder() - children(newUI) // execute - box.widgets.addAll(newUI.build()) - instructions.addAll(box.toInstruction()) - return box -} - -fun dummy() { - val instructions = buildUI { - colouredBox(WidgetId("something"), Colour.TRANSPARENT, Vector2d.zero()) - - colouredBoxContainer(WidgetId("something"), Colour.TRANSPARENT) { - - } - } -} @@ -1,12 +0,0 @@ -package com.dropbear.asset; - -import com.dropbear.EucalyptusCoreLoader; - -public class AssetHandleNative { - static { - new EucalyptusCoreLoader().ensureLoaded(); - } - - public static native boolean isModelHandle(long assetRegistryHandle, long handle); - public static native boolean isTextureHandle(long assetRegistryHandle, long handle); -} @@ -1,11 +0,0 @@ -package com.dropbear.asset; - -import com.dropbear.EucalyptusCoreLoader; - -public class TextureHandleNative { - static { - new EucalyptusCoreLoader().ensureLoaded(); - } - - public static native String getTextureName(long assetRegistryHandle, long handle); -} @@ -1,11 +0,0 @@ -package com.dropbear.asset - -import com.dropbear.DropbearEngine - -internal actual fun isModelHandle(id: Long): Boolean { - return AssetHandleNative.isModelHandle(DropbearEngine.native.assetHandle, id) -} - -internal actual fun isTextureHandle(id: Long): Boolean { - return AssetHandleNative.isTextureHandle(DropbearEngine.native.assetHandle, id) -} @@ -1,7 +0,0 @@ -package com.dropbear.asset - -import com.dropbear.DropbearEngine - -internal actual fun TextureHandle.getTextureName(id: Long): String? { - return TextureHandleNative.getTextureName(DropbearEngine.native.assetHandle, id) -} @@ -2,7 +2,6 @@ package com.dropbear.components import com.dropbear.DropbearEngine import com.dropbear.EntityId -import com.dropbear.asset.ModelHandle import com.dropbear.asset.TextureHandle internal actual fun MeshRenderer.getModel(id: EntityId): ModelHandle? { @@ -1,9 +0,0 @@ -package com.dropbear.asset - -internal actual fun isModelHandle(id: Long): Boolean { - TODO("Not yet implemented") -} - -internal actual fun isTextureHandle(id: Long): Boolean { - TODO("Not yet implemented") -} @@ -1,5 +0,0 @@ -package com.dropbear.asset - -internal actual fun TextureHandle.getTextureName(id: Long): String? { - TODO("Not yet implemented") -} @@ -1,7 +1,6 @@ package com.dropbear.components import com.dropbear.EntityId -import com.dropbear.asset.ModelHandle import com.dropbear.asset.TextureHandle internal actual fun MeshRenderer.getModel(id: EntityId): ModelHandle? {