tirbofish/dropbear · diff
Merge pull request #89 from tirbofish/patch
patch + feature: asset server and fixing ui
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); + } + } +} @@ -1,513 +1,229 @@ -use std::sync::{ - Arc, LazyLock, - atomic::{AtomicU64, Ordering}, -}; - -use dashmap::DashMap; - +use std::collections::HashMap; +use std::hash::{DefaultHasher, Hash, Hasher}; +use std::marker::PhantomData; +use std::sync::{Arc, LazyLock}; +use parking_lot::RwLock; use crate::{ - graphics::{SharedGraphicsContext}, - model::{Material, Mesh, Model, ModelId}, - utils::ResourceReference, texture::Texture, }; +use crate::graphics::SharedGraphicsContext; +use crate::model::Model; -/// Opaque identifier returned from the [`AssetRegistry`] for stored assets. -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] -pub struct AssetHandle(u64); -impl AssetHandle { - /// Creates a new [`AssetHandle`]. - /// - /// This function does not guarantee if the raw value exists in the registry. - /// You will have to check yourself. - pub fn new(raw: impl Into<u64>) -> Self { - Self(raw.into()) - } - /// Returns the raw/primitive [`u64`] value. - pub fn raw(&self) -> u64 { - self.0 - } -} - -#[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub enum AssetKind { - Model, - Material, - Mesh, -} +pub static ASSET_REGISTRY: LazyLock<Arc<RwLock<AssetRegistry>>> = LazyLock::new(|| Arc::new(RwLock::new(AssetRegistry::new()))); -#[derive(Debug, Eq, PartialEq, Hash)] -pub enum PointerKind { - Const(&'static str), - Mut(&'static str), +/// A handle with type [`T`] that provides an index to the [AssetRegistry] contents. +#[derive(Hash, Eq, PartialEq, Debug)] +pub struct Handle<T> { + pub id: u64, + _phantom: PhantomData<T> } -/// Centralised cache for models and their dependent resources. -/// -/// The registry assigns stable [`AssetHandle`] values that can be -/// reused by systems without having to keep strong references to the -/// underlying assets. Models are keyed by their [`ResourceReference`] -/// while meshes and materials are keyed by `(ModelId, name)` pairs. -pub struct AssetRegistry { - next_id: AtomicU64, - - model_handles: DashMap<ResourceReference, AssetHandle>, - model_id_lookup: DashMap<ModelId, AssetHandle>, - model_references: DashMap<AssetHandle, ResourceReference>, - model_reference_lookup: DashMap<ResourceReference, AssetHandle>, - models: DashMap<AssetHandle, Arc<Model>>, +impl<T> Copy for Handle<T> {} - material_lookup: DashMap<(ModelId, String), AssetHandle>, - material_owners: DashMap<AssetHandle, ModelId>, - material_references: DashMap<AssetHandle, ResourceReference>, - material_reference_lookup: DashMap<ResourceReference, AssetHandle>, - materials: DashMap<AssetHandle, Arc<Material>>, +impl<T> Clone for Handle<T> { + fn clone(&self) -> Self { + *self + } +} - mesh_lookup: DashMap<(ModelId, String), AssetHandle>, - mesh_owners: DashMap<AssetHandle, ModelId>, - mesh_references: DashMap<AssetHandle, ResourceReference>, - mesh_reference_lookup: DashMap<ResourceReference, AssetHandle>, - meshes: DashMap<AssetHandle, Arc<Mesh>>, +impl<T> Handle<T> { + /// Creates a null handle, for when there is no way to uniquely identify a hash (such as a viewport texture). + /// + /// # Safety + /// You will want to watch out, as adding this onto the asset registry with a type + /// where there already is a null handle item, it will be overwritten and data + /// will not be saved. It is the reason why you will want to consider using the [Self::is_null] + /// function to verify if the storage of the type has gone through correctly. + pub const NULL: Self = Self { id: 0, _phantom: PhantomData }; - /// Internal pointer database, typically used when querying in the database - pointers: DashMap<PointerKind, usize>, + /// Creates a new handle with the given ID. + pub fn new(id: u64) -> Self { + Self { id, _phantom: Default::default() } + } - /// Built-in textures created at runtime and reused across assets. - built_in_textures: DashMap<&'static str, Arc<Texture>>, + /// Returns true if the handle is null. + pub fn is_null(&self) -> bool { + self.id == 0 + } +} - /// 1×1 solid-colour textures cached by packed RGBA8. - solid_textures: DashMap<u32, Arc<Texture>>, +pub struct AssetRegistry { + textures: HashMap<u64, Texture>, + texture_labels: HashMap<String, Handle<Texture>>, - /// Per-model import-scale overrides keyed by the model's resource reference. - /// - /// This is editor/user configuration and should outlive cached model data. - model_import_scales: DashMap<ResourceReference, f32>, + models: HashMap<u64, Model>, + model_labels: HashMap<String, Handle<Model>>, } - +/// Common impl AssetRegistry { pub fn new() -> Self { Self { - next_id: AtomicU64::new(1), - model_handles: DashMap::new(), - model_id_lookup: DashMap::new(), - model_references: DashMap::new(), - model_reference_lookup: DashMap::new(), - models: DashMap::new(), - material_lookup: DashMap::new(), - material_owners: DashMap::new(), - material_references: DashMap::new(), - material_reference_lookup: DashMap::new(), - materials: DashMap::new(), - mesh_lookup: DashMap::new(), - mesh_owners: DashMap::new(), - mesh_references: DashMap::new(), - mesh_reference_lookup: DashMap::new(), - meshes: DashMap::new(), - pointers: DashMap::new(), - built_in_textures: DashMap::new(), - solid_textures: DashMap::new(), - model_import_scales: DashMap::new(), + textures: Default::default(), + texture_labels: Default::default(), + models: Default::default(), + model_labels: Default::default(), } } - /// Returns the per-model import scale for the given resource reference. - /// - /// Defaults to `1.0` when no override exists. - pub fn model_import_scale(&self, reference: &ResourceReference) -> f32 { - self.model_import_scales - .get(reference) - .map(|v| *v.value()) - .unwrap_or(1.0) + /// A convenient helper function for hashing a byte slice of data. + pub(crate) fn hash_bytes(data: &[u8]) -> u64 { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() } - /// Sets the per-model import scale override for the given resource reference. - pub fn set_model_import_scale(&self, reference: ResourceReference, scale: f32) { - let scale = if scale.is_finite() { scale } else { 1.0 }; - self.model_import_scales.insert(reference, scale); + /// A convenient helper function for hashing a byte slice of data. + pub(crate) fn hash_contents<T: Hash>(data: T) -> u64 { + let mut hasher = DefaultHasher::new(); + data.hash(&mut hasher); + hasher.finish() } - /// Returns a cached 1×1 solid-colour texture, creating it on first use. + /// Checks if the asset registry contains a handle with the given hash. /// - /// The cache key is the packed RGBA8 bytes (little-endian). - pub fn solid_texture_rgba8( - &self, - graphics: Arc<SharedGraphicsContext>, - rgba: [u8; 4], - ) -> Arc<Texture> { - let key = u32::from_le_bytes(rgba); - - if let Some(existing) = self.solid_textures.get(&key) { - return Arc::clone(existing.value()); - } - - let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]); - - let texture = Texture::from_bytes_verbose_mipmapped( - graphics, - &rgba, - Some((1, 1)), - None, - None, - Some(label.as_str()) - ); - let texture = Arc::new(texture); - - match self.solid_textures.entry(key) { - dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()), - dashmap::mapref::entry::Entry::Vacant(entry) => { - entry.insert(Arc::clone(&texture)); - texture - } - } + /// It will check all different types, so it does not point out specifically where. + pub fn contains_hash(&self, hash: u64) -> bool { + self.textures.contains_key(&hash) || self.models.contains_key(&hash) } - /// Returns a cached 1×1 grey texture, creating it on first use. + /// Checks if the asset registry contains a handle with the given string label. /// - /// This is intended as a cheap fallback when a model/material has no diffuse texture. - pub fn grey_texture(&self, graphics: Arc<SharedGraphicsContext>) -> Arc<Texture> { - const KEY: &str = "builtin_grey_1x1"; - - if let Some(existing) = self.built_in_textures.get(KEY) { - return Arc::clone(existing.value()); - } - - // 50% grey, fully opaque. - let texture = self.solid_texture_rgba8(graphics, [128, 128, 128, 255]); - - // Race-safe insert: if another thread beat us, reuse theirs. - match self.built_in_textures.entry(KEY) { - dashmap::mapref::entry::Entry::Occupied(entry) => Arc::clone(entry.get()), - dashmap::mapref::entry::Entry::Vacant(entry) => { - entry.insert(Arc::clone(&texture)); - texture - } - } + /// It will check all different types, so it does not point out specifically where. + pub fn contains_label(&self, label: &str) -> bool { + self.texture_labels.contains_key(label) || self.model_labels.contains_key(label) } +} - /// Clears all cached asset data (models/materials/meshes) from the registry. +/// Texture stuff +impl AssetRegistry { + /// Adds a texture and returns a handle. /// - /// This is intended for full scene reloads where the previous scene's assets - /// should be dropped and reloaded. Pointer entries are intentionally preserved. - pub fn clear_cached_assets(&self) { - self.model_handles.clear(); - self.model_id_lookup.clear(); - self.model_references.clear(); - self.model_reference_lookup.clear(); - self.models.clear(); - - self.material_lookup.clear(); - self.material_owners.clear(); - self.material_references.clear(); - self.material_reference_lookup.clear(); - self.materials.clear(); - - self.mesh_lookup.clear(); - self.mesh_owners.clear(); - self.mesh_references.clear(); - self.mesh_reference_lookup.clear(); - self.meshes.clear(); - } - - /// Adds a pointer to the asset registry. - pub fn add_pointer(&self, pointer_kind: PointerKind, pointer: usize) { - self.pointers.insert(pointer_kind, pointer); - } - - /// Attempts to fetch a pointer from the [`AssetRegistry`] by its given [`PointerKind`] - pub fn get_pointer(&self, pointer_kind: PointerKind) -> Option<usize> { - self.pointers.get(&pointer_kind).map(|entry| *entry.value()) - } - - fn allocate_handle(&self) -> AssetHandle { - AssetHandle(self.next_id.fetch_add(1, Ordering::Relaxed)) - } - - /// Registers a model and caches its meshes and materials. - pub fn register_model(&self, reference: ResourceReference, model: Arc<Model>) -> AssetHandle { - let canonical = reference.clone(); - self.model_import_scales.entry(canonical.clone()).or_insert(1.0); - - let model_handle = if let Some(existing) = self.model_handles.get(&canonical) { - let handle = *existing; - self.models.insert(handle, Arc::clone(&model)); - handle - } else { - let handle = self.allocate_handle(); - self.models.insert(handle, Arc::clone(&model)); - self.model_handles.insert(canonical.clone(), handle); - handle - }; - - self.model_id_lookup.insert(model.id, model_handle); - - self.model_references - .insert(model_handle, canonical.clone()); - self.model_reference_lookup.insert(canonical, model_handle); - - self.cache_model_components(&model); - - model_handle - } - - /// Iterates through all models, allowing you to iterate through all items in the - /// model registry. - pub fn iter_model(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> { - self.iter_model_raw() + /// This assumes a [Texture] has already been created by you. To create a new texture, + /// you can use [`Texture::from_bytes`]. + pub fn add_texture(&mut self, texture: Texture) -> Handle<Texture> { + let handle = texture.hash.map(|v| Handle::new(v)).unwrap_or_else(|| Handle::NULL); + self.textures.entry(handle.id).or_insert(texture); + handle } - pub fn iter_model_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> { - self.models.iter() + /// Adds a texture with a label. If the texture already exists (by hash), + /// returns the existing handle and updates the label to point at it. + pub fn add_texture_with_label(&mut self, label: impl Into<String>, texture: Texture) -> Handle<Texture> { + let handle = self.add_texture(texture); + self.texture_labels.insert(label.into(), handle.clone()); + handle } - pub fn iter_material(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> { - self.iter_material_raw() + /// Maps a label to an existing texture handle. + pub fn label_texture(&mut self, label: impl Into<String>, handle: Handle<Texture>) { + self.texture_labels.insert(label.into(), handle.clone()); } - pub fn iter_material_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> { - self.materials.iter() - } - - /// Returns the cached model handle if it exists. - pub fn model_handle(&self, reference: &ResourceReference) -> Option<AssetHandle> { - self.model_handle_raw(reference) - } - - pub fn model_handle_raw(&self, reference: &ResourceReference) -> Option<AssetHandle> { - self.model_handles.get(reference).map(|entry| *entry) - } - - /// Fetches a model by handle. - pub fn get_model(&self, handle: AssetHandle) -> Option<Arc<Model>> { - self.get_model_raw(handle) - } - - pub fn get_model_raw(&self, handle: AssetHandle) -> Option<Arc<Model>> { - self.models - .get(&handle) - .map(|entry| Arc::clone(entry.value())) - } - - /// Fetches a material by handle. - pub fn get_material(&self, handle: AssetHandle) -> Option<Arc<Material>> { - self.get_material_raw(handle) - } - - pub fn get_material_raw(&self, handle: AssetHandle) -> Option<Arc<Material>> { - self.materials - .get(&handle) - .map(|entry| Arc::clone(entry.value())) + /// 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); } - /// Fetches a mesh by handle. - pub fn get_mesh(&self, handle: AssetHandle) -> Option<Arc<Mesh>> { - self.get_mesh_raw(handle) + /// 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> { + self.textures.insert(handle.id, texture) } - pub fn get_mesh_raw(&self, handle: AssetHandle) -> Option<Arc<Mesh>> { - self.meshes - .get(&handle) - .map(|entry| Arc::clone(entry.value())) + pub fn get_texture(&self, handle: Handle<Texture>) -> Option<&Texture> { + self.textures.get(&handle.id) } - /// Fetches a handle from a [`ResourceReference`] by checking through - /// each cache - pub fn get_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> { - self.material_handle_from_reference(reference) - .or_else(|| self.mesh_handle_from_reference(reference)) - .or_else(|| self.model_handle_from_reference(reference)) + pub fn get_texture_by_label(&self, label: &str) -> Option<&Texture> { + self.texture_labels + .get(label) + .and_then(|handle| self.textures.get(&handle.id)) } - /// Retrieves (or lazily creates) the handle for a specific material on a model. - pub fn material_handle(&self, model_id: ModelId, name: &str) -> Option<AssetHandle> { - let key = (model_id, name.to_string()); - self.material_lookup.get(&key).map(|entry| *entry) + pub fn get_texture_handle_from_label(&self, label: &str) -> Option<Handle<Texture>> { + self.texture_labels.get(label).cloned() } - /// Retrieves (or lazily creates) the handle for a specific mesh on a model. - pub fn mesh_handle(&self, model_id: ModelId, name: &str) -> Option<AssetHandle> { - let key = (model_id, name.to_string()); - self.mesh_lookup.get(&key).map(|entry| *entry) + pub fn texture_handle_by_hash(&self, hash: u64) -> Option<Handle<Texture>> { + self.textures.contains_key(&hash).then(|| Handle::new(hash)) } - /// Returns the kind of asset associated with a handle, if known. - pub fn kind(&self, handle: AssetHandle) -> Option<AssetKind> { - if self.models.contains_key(&handle) { - Some(AssetKind::Model) - } else if self.materials.contains_key(&handle) { - Some(AssetKind::Material) - } else if self.meshes.contains_key(&handle) { - Some(AssetKind::Mesh) - } else { - None + pub fn grey_texture(&mut self, graphics: Arc<SharedGraphicsContext>) -> Handle<Texture> { + let grey_handle = Handle::new(Self::hash_contents("Solid texture [128, 128, 128, 255]")); + + if self.contains_hash(grey_handle.id) { + return grey_handle; } - } - - /// Returns `true` if the handle exists in any asset cache. - pub fn contains_handle(&self, handle: AssetHandle) -> bool { - self.models.contains_key(&handle) - || self.materials.contains_key(&handle) - || self.meshes.contains_key(&handle) - } - - /// Returns `true` if the handle represents the expected asset kind. - pub fn is_handle_kind(&self, handle: AssetHandle, expected: AssetKind) -> bool { - matches!(self.kind(handle), Some(kind) if kind == expected) - } - - /// Returns the `ResourceReference` recorded for a model handle, if any. - pub fn model_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> { - self.model_references - .get(&handle) - .map(|entry| entry.value().clone()) - } - - /// Attempts to resolve a model handle from a `ResourceReference`. - pub fn model_handle_from_reference( - &self, - reference: &ResourceReference, - ) -> Option<AssetHandle> { - self.model_reference_lookup - .get(reference) - .map(|entry| *entry) - } - - /// Attempts to resolve a model handle directly from a [`ModelId`]. - pub fn model_handle_from_id(&self, model_id: ModelId) -> Option<AssetHandle> { - self.model_id_lookup.get(&model_id).map(|entry| *entry) - } - - /// Returns `true` if the handle refers to a material asset. - pub fn is_material(&self, handle: AssetHandle) -> bool { - self.materials.contains_key(&handle) - } - /// Returns `true` if the handle refers to a mesh asset. - pub fn is_mesh(&self, handle: AssetHandle) -> bool { - self.meshes.contains_key(&handle) + self.solid_texture_rgba8(graphics, [128, 128, 128, 255]) } - /// Returns `true` if the handle refers to a model asset. - pub fn is_model(&self, handle: AssetHandle) -> bool { - self.models.contains_key(&handle) - } - - /// Returns the owning model ID for the given material handle. - pub fn material_owner(&self, handle: AssetHandle) -> Option<ModelId> { - self.material_owner_raw(handle) - } + pub fn solid_texture_rgba8( + &mut self, + graphics: Arc<SharedGraphicsContext>, + rgba: [u8; 4], + ) -> Handle<Texture> { + let handle = Handle::new(Self::hash_bytes(&rgba)); - pub fn material_owner_raw(&self, handle: AssetHandle) -> Option<ModelId> { - self.material_owners.get(&handle).map(|entry| *entry) - } + if self.contains_hash(handle.id) { + return handle; + } - /// Returns the owning model ID for the given mesh handle. - pub fn mesh_owner(&self, handle: AssetHandle) -> Option<ModelId> { - self.mesh_owners.get(&handle).map(|entry| *entry) - } + let label = format!("Solid texture [{}, {}, {}, {}]", rgba[0], rgba[1], rgba[2], rgba[3]); - /// Returns the synthetic `ResourceReference` associated with a material handle. - pub fn material_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> { - self.material_reference_for_handle_raw(handle) + let texture = Texture::from_bytes_verbose_mipmapped( + graphics, + &rgba, + Some((1, 1)), + None, + None, + Some(label.as_str()) + ); + + self.add_texture_with_label(label, texture) } +} - pub fn material_reference_for_handle_raw( - &self, - handle: AssetHandle, - ) -> Option<ResourceReference> { - self.material_references - .get(&handle) - .map(|entry| entry.value().clone()) +/// Model stuff +impl AssetRegistry { + pub fn add_model(&mut self, model: Model) -> Handle<Model> { + let handle = Handle::new(model.hash); + self.models.entry(handle.id).or_insert(model); + handle } - /// Returns the synthetic `ResourceReference` associated with a mesh handle. - pub fn mesh_reference_for_handle(&self, handle: AssetHandle) -> Option<ResourceReference> { - self.mesh_reference_for_handle_raw(handle) + pub fn add_model_with_label(&mut self, label: impl Into<String>, model: Model) -> Handle<Model> { + let handle = self.add_model(model); + self.model_labels.insert(label.into(), handle.clone()); + handle } - pub fn mesh_reference_for_handle_raw(&self, handle: AssetHandle) -> Option<ResourceReference> { - self.mesh_references - .get(&handle) - .map(|entry| entry.value().clone()) + pub fn label_model(&mut self, label: impl Into<String>, handle: Handle<Model>) { + self.model_labels.insert(label.into(), handle.clone()); } - /// Attempts to resolve a material handle from its synthetic `ResourceReference`. - pub fn material_handle_from_reference( - &self, - reference: &ResourceReference, - ) -> Option<AssetHandle> { - self.material_handle_from_reference_raw(reference) + pub fn update_model(&mut self, handle: Handle<Model>, model: Model) -> Option<Model> { + self.models.insert(handle.id, model) } - pub fn material_handle_from_reference_raw( - &self, - reference: &ResourceReference, - ) -> Option<AssetHandle> { - self.material_reference_lookup - .get(reference) - .map(|entry| *entry) + pub fn get_model(&self, handle: Handle<Model>) -> Option<&Model> { + self.models.get(&handle.id) } - /// Attempts to resolve a mesh handle from its synthetic `ResourceReference`. - pub fn mesh_handle_from_reference(&self, reference: &ResourceReference) -> Option<AssetHandle> { - self.mesh_handle_from_reference_raw(reference) + pub fn get_model_by_label(&self, label: &str) -> Option<&Model> { + self.model_labels + .get(label) + .and_then(|handle| self.models.get(&handle.id)) } - pub fn mesh_handle_from_reference_raw( - &self, - reference: &ResourceReference, - ) -> Option<AssetHandle> { - self.mesh_reference_lookup - .get(reference) - .map(|entry| *entry) + pub fn get_model_handle_from_label(&self, label: &str) -> Option<Handle<Model>> { + self.model_labels.get(label).cloned() } - fn cache_model_components(&self, model: &Arc<Model>) { - let model_id = model.id; - - for material in &model.materials { - let name = material.name.clone(); - let key = (model_id, name.clone()); - let handle = if let Some(existing) = self.material_lookup.get(&key) { - *existing - } else { - let handle = self.allocate_handle(); - self.material_lookup.insert(key.clone(), handle); - handle - }; - - self.material_owners.insert(handle, model_id); - - let reference = material_reference_from_model(model.as_ref(), &name) - .or_else(|| material_reference_fallback(model_id, &name)); - - if let Some(reference) = reference { - self.material_references.insert(handle, reference.clone()); - self.material_reference_lookup.insert(reference, handle); - } - - self.materials.insert(handle, Arc::new(material.clone())); - } - - for mesh in &model.meshes { - let name = mesh.name.clone(); - let key = (model_id, name.clone()); - let handle = if let Some(existing) = self.mesh_lookup.get(&key) { - *existing - } else { - let handle = self.allocate_handle(); - self.mesh_lookup.insert(key.clone(), handle); - handle - }; - - self.mesh_owners.insert(handle, model_id); - - if let Some(reference) = mesh_reference(model_id, &name) { - self.mesh_references.insert(handle, reference.clone()); - self.mesh_reference_lookup.insert(reference, handle); - } - - self.meshes.insert(handle, Arc::new(mesh.clone())); - } + pub fn model_handle_by_hash(&self, hash: u64) -> Option<Handle<Model>> { + self.models.contains_key(&hash).then(|| Handle::new(hash)) } } @@ -515,74 +231,4 @@ impl Default for AssetRegistry { fn default() -> Self { Self::new() } -} - -pub static ASSET_REGISTRY: LazyLock<AssetRegistry> = LazyLock::new(AssetRegistry::new); - -fn material_reference_from_model(model: &Model, name: &str) -> Option<ResourceReference> { - let base_uri = model.path.as_uri()?; - let material_component = sanitize_material_component(name); - - if material_component.is_empty() { - return None; - } - - let base = base_uri.trim_end_matches('/'); - let combined = format!("{}/{}", base, material_component); - - ResourceReference::from_euca_uri(combined).ok() -} - -fn material_reference_fallback(model_id: ModelId, name: &str) -> Option<ResourceReference> { - resource_reference_for("materials", model_id, name) -} - -fn mesh_reference(model_id: ModelId, name: &str) -> Option<ResourceReference> { - resource_reference_for("meshes", model_id, name) -} - -fn resource_reference_for( - category: &str, - model_id: ModelId, - name: &str, -) -> Option<ResourceReference> { - let sanitized = sanitize_component(name); - if sanitized.is_empty() { - return None; - } - let uri = format!("euca://{}/{}/{}", category, model_id.raw(), sanitized); - ResourceReference::from_euca_uri(uri).ok() -} - -fn sanitize_material_component(input: &str) -> String { - let trimmed = input.trim(); - if trimmed.is_empty() { - return String::new(); - } - - trimmed - .chars() - .map(|ch| match ch { - 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '.' => ch, - _ => '_', - }) - .collect() -} - -fn sanitize_component(input: &str) -> String { - let trimmed = input.trim(); - if trimmed.is_empty() { - return String::new(); - } - - trimmed - .chars() - .map(|ch| { - let lower = ch.to_ascii_lowercase(); - match lower { - 'a'..='z' | '0'..='9' | '-' | '_' => lower, - _ => '_', - } - }) - .collect() -} +} @@ -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,6 +12,9 @@ use glam::{DMat4, DVec3}; use std::fmt::{Display, Formatter}; use std::sync::Arc; use wgpu::{BindGroup}; +use crate::asset::{Handle, ASSET_REGISTRY}; +use crate::model::Material; +use crate::procedural::{ProcObj, ProcedurallyGeneratedObject}; pub const MAX_LIGHTS: usize = 10; @@ -240,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, @@ -270,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; @@ -288,15 +292,13 @@ impl Light { log::trace!("Created new light uniform"); - let cube_model = Model::load_from_memory( - graphics.clone(), - include_bytes!("../../../resources/models/cube.glb").to_vec(), - label, - 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(); @@ -337,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); @@ -359,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() { @@ -1,13 +1,12 @@ -use crate::asset::AssetRegistry; +use crate::asset::{AssetRegistry, Handle}; use crate::buffer::UniformBuffer; use crate::{ - asset::{ASSET_REGISTRY, AssetHandle}, graphics::{SharedGraphicsContext}, utils::{ResourceReference}, texture::{Texture, TextureWrapMode} }; -use image::GenericImageView; -use parking_lot::Mutex; +// use image::GenericImageView; +use parking_lot::{Mutex, RwLock}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -16,178 +15,126 @@ use std::ops::Deref; use std::sync::{Arc, LazyLock}; use std::time::Instant; use std::{mem, ops::Range, path::PathBuf}; -use glam::{Vec2, Vec3}; +use gltf::image::Format; +use gltf::texture::MinFilter; +use puffin::profile_scope; use wgpu::{BufferAddress, VertexAttribute, VertexBufferLayout, util::DeviceExt, BindGroup}; -pub static MODEL_CACHE: LazyLock<Mutex<HashMap<String, Arc<Model>>>> = - LazyLock::new(|| Mutex::new(HashMap::new())); - -#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] -pub struct ModelId(pub u64); - -impl ModelId { - pub fn raw(&self) -> u64 { - self.0 - } -} - #[derive(Clone)] pub struct Model { + pub(crate) hash: u64, // also the id related to the handle pub label: String, pub path: ResourceReference, pub meshes: Vec<Mesh>, pub materials: Vec<Material>, - pub id: ModelId, + pub skins: Vec<Skin>, + pub animations: Vec<Animation>, + pub nodes: Vec<Node>, } -/// A shared, GPU-backed renderable shape. -/// -/// This is intentionally a lightweight wrapper around an `Arc<Model>`. -/// It exists so other systems can treat both model-loaded and procedurally -/// generated geometry uniformly, while retaining clone-on-write semantics. #[derive(Clone)] -pub struct SharedShape { - model: Arc<Model>, +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>, } -impl SharedShape { - pub fn new(model: Arc<Model>) -> Self { - Self { model } - } - - pub fn get(&self) -> Arc<Model> { - Arc::clone(&self.model) - } - - pub fn make_mut(&mut self) -> &mut Model { - Arc::make_mut(&mut self.model) - } +#[derive(Clone)] +pub struct Material { + pub name: String, + pub diffuse_texture: Texture, + pub normal_texture: Texture, + pub bind_group: wgpu::BindGroup, + pub tint: [f32; 4], + pub emissive_factor: [f32; 3], + pub metallic_factor: f32, + pub roughness_factor: f32, + pub alpha_mode: gltf::material::AlphaMode, + pub alpha_cutoff: Option<f32>, + pub double_sided: bool, + pub occlusion_strength: f32, + pub normal_scale: f32, + pub uv_tiling: [f32; 2], + pub tint_buffer: UniformBuffer<MaterialUniform>, + pub texture_tag: Option<String>, + pub wrap_mode: TextureWrapMode, + pub emissive_texture: Option<Texture>, + pub metallic_roughness_texture: Option<Texture>, + pub occlusion_texture: Option<Texture>, } -impl Deref for SharedShape { - type Target = Model; - - fn deref(&self) -> &Self::Target { - self.model.as_ref() - } +/// 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, } -#[derive(Clone)] -pub struct LoadedModel { - pub(crate) inner: Arc<SharedShape>, - handle: AssetHandle, +/// 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 LoadedModel { - pub fn new(inner: Arc<Model>) -> Self { - Self::new_raw(&ASSET_REGISTRY, inner) - } - - pub fn new_raw(registry: &AssetRegistry, inner: Arc<Model>) -> Self { - let reference = inner.path.clone(); - let handle = registry.register_model(reference, Arc::clone(&inner)); - let inner = Arc::new(SharedShape::new(inner)); - Self { inner, handle } - } - - pub fn from_registered(handle: AssetHandle, inner: Arc<Model>) -> Self { - let inner = Arc::new(SharedShape::new(inner)); - Self { inner, handle } - } - - pub fn from_asset_handle_raw(registry: &AssetRegistry, handle: AssetHandle) -> Option<Self> { - registry - .get_model(handle) - .map(|model| Self::from_registered(handle, model)) - } - - pub fn from_asset_handle(handle: AssetHandle) -> Option<Arc<LoadedModel>> { - Self::from_asset_handle_raw(&ASSET_REGISTRY, handle).map(|model| Arc::new(model)) - } - - /// Returns the unique identifier of the underlying model asset. - pub fn id(&self) -> ModelId { - self.inner.id - } - - /// Returns the asset handle associated with the underlying model. - pub fn asset_handle(&self) -> AssetHandle { - self.handle +impl NodeTransform { + pub fn to_matrix(&self) -> glam::Mat4 { + glam::Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation) } - pub fn matches_resource(&self, reference: &ResourceReference) -> bool { - self.inner.matches_resource(reference) - } - - /// Provides shared access to the underlying model. - pub fn get(&self) -> Arc<Model> { - self.inner.get() - } - - /// Provides mutable access to the underlying model data, cloning if shared. - pub fn make_mut(&mut self) -> &mut Model { - let shape = Arc::make_mut(&mut self.inner); - shape.make_mut() - } - - /// Re-registers the model with the global asset registry, ensuring cached - /// sub-assets stay in sync after mutations. - pub fn refresh_registry(&mut self) { - self.refresh_registry_raw(&ASSET_REGISTRY); - } - - pub fn refresh_registry_raw(&mut self, registry: &AssetRegistry) { - let reference = self.inner.path.clone(); - let updated_handle = registry.register_model(reference, self.get()); - self.handle = updated_handle; - } - - pub fn contains_material_handle(&self, handle: AssetHandle) -> bool { - self.contains_material_handle_raw(&ASSET_REGISTRY, handle) - } - - pub fn contains_material_handle_raw( - &self, - registry: &AssetRegistry, - handle: AssetHandle, - ) -> bool { - self.inner.contains_material_handle_raw(registry, handle) - } - - pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool { - self.contains_material_reference_raw(&ASSET_REGISTRY, reference) + pub fn identity() -> Self { + Self { + translation: glam::Vec3::ZERO, + rotation: glam::Quat::IDENTITY, + scale: glam::Vec3::ONE, + } } +} - pub fn contains_material_reference_raw( - &self, - registry: &AssetRegistry, - reference: &ResourceReference, - ) -> bool { - self.inner - .contains_material_reference_raw(registry, reference) - } +/// 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>, } -impl std::ops::Deref for LoadedModel { - type Target = Model; +/// An animation that can be played on a skeleton +#[derive(Clone)] +pub struct Animation { + pub name: String, + pub channels: Vec<AnimationChannel>, + pub duration: f32, +} - fn deref(&self) -> &Self::Target { - self.inner.deref() - } +/// 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 struct Material { - pub name: String, - pub diffuse_texture: Texture, - pub normal_texture: Texture, - pub bind_group: wgpu::BindGroup, - pub tint: [f32; 4], - 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 enum ChannelValues { + Translations(Vec<glam::Vec3>), + Rotations(Vec<glam::Quat>), + Scales(Vec<glam::Vec3>), } impl Material { @@ -196,39 +143,30 @@ impl Material { name: impl Into<String>, diffuse_texture: Texture, normal_texture: Texture, - ) -> Self { - Self::new_with_tint(graphics, name, diffuse_texture, normal_texture, [1.0, 1.0, 1.0, 1.0], None) - } - - pub fn new_with_tint( - graphics: Arc<SharedGraphicsContext>, - name: impl Into<String>, - diffuse_texture: Texture, - normal_texture: Texture, tint: [f32; 4], texture_tag: Option<String>, ) -> Self { - let name: String = name.into(); + puffin::profile_function!(); + let name = name.into(); let uv_tiling = [1.0, 1.0]; let uniform = MaterialUniform { - colour: tint, + base_colour: tint, + emissive: [0.0, 0.0, 0.0], + emissive_strength: 1.0, + metallic: 1.0, + roughness: 1.0, + normal_scale: 1.0, + occlusion_strength: 1.0, + alpha_cutoff: 0.5, uv_tiling, - _pad: [0.0, 0.0], + _pad: 0.0, }; - + 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, @@ -236,11 +174,21 @@ impl Material { normal_texture, bind_group, tint, + emissive_factor: [0.0, 0.0, 0.0], + metallic_factor: 1.0, + roughness_factor: 1.0, + alpha_mode: gltf::material::AlphaMode::Opaque, + alpha_cutoff: None, + double_sided: false, + occlusion_strength: 1.0, + normal_scale: 1.0, uv_tiling, tint_buffer, - tint_bind_group, texture_tag, wrap_mode: TextureWrapMode::Repeat, + emissive_texture: None, + metallic_roughness_texture: None, + occlusion_texture: None, } } @@ -248,376 +196,748 @@ impl Material { graphics: &SharedGraphicsContext, diffuse: &Texture, normal: &Texture, + uniform_buffer: &UniformBuffer<MaterialUniform>, name: &str, ) -> BindGroup { - graphics. - device - .create_bind_group(&wgpu::BindGroupDescriptor { - label: Some(format!("{} mipmapped compute bind group", name).as_str()), - layout: &graphics.layouts.texture_bind_layout, - entries: &[ - wgpu::BindGroupEntry { - binding: 0, - resource: wgpu::BindingResource::TextureView( - &diffuse.view, - ), - }, - wgpu::BindGroupEntry { - binding: 1, - resource: wgpu::BindingResource::Sampler(&diffuse.sampler), - }, - wgpu::BindGroupEntry { - binding: 2, - resource: wgpu::BindingResource::TextureView(&normal.view), - }, - wgpu::BindGroupEntry { - binding: 3, - resource: wgpu::BindingResource::Sampler(&normal.sampler), - }, - ], - }) + puffin::profile_function!(); + graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some(format!("{} texture bind group", name).as_str()), + layout: &graphics.layouts.material_bind_layout, + entries: &[ + wgpu::BindGroupEntry { + binding: 0, + resource: wgpu::BindingResource::TextureView(&diffuse.view), + }, + wgpu::BindGroupEntry { + binding: 1, + resource: wgpu::BindingResource::Sampler(&diffuse.sampler), + }, + wgpu::BindGroupEntry { + binding: 2, + resource: wgpu::BindingResource::TextureView(&normal.view), + }, + wgpu::BindGroupEntry { + binding: 3, + resource: wgpu::BindingResource::Sampler(&normal.sampler), + }, + wgpu::BindGroupEntry { + binding: 4, + resource: uniform_buffer.buffer().as_entire_binding(), + }, + ], + }) } - pub fn set_tint(&mut self, graphics: &SharedGraphicsContext, tint: [f32; 4]) { - self.tint = tint; + pub fn sync_uniform(&self, graphics: &SharedGraphicsContext) { let uniform = MaterialUniform { - colour: tint, + base_colour: self.tint, + emissive: self.emissive_factor, + emissive_strength: 1.0, + metallic: self.metallic_factor, + roughness: self.roughness_factor, + normal_scale: self.normal_scale, + occlusion_strength: self.occlusion_strength, + alpha_cutoff: self.alpha_cutoff.unwrap_or(0.5), uv_tiling: self.uv_tiling, - _pad: [0.0, 0.0], + _pad: 0.0, }; self.tint_buffer.write(&graphics.queue, &uniform); } +} - pub fn set_uv_tiling(&mut self, graphics: &SharedGraphicsContext, tiling: [f32; 2]) { - self.uv_tiling = tiling; - let uniform = MaterialUniform { - colour: self.tint, - uv_tiling: tiling, - _pad: [0.0, 0.0], - }; - self.tint_buffer.write(&graphics.queue, &uniform); - } +#[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, } -#[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>, +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, } -impl Model { - /// Replaces the material textures (diffuse + normal) for the material identified by `material_name`. - /// - /// Note: `bind_group` must match the provided textures. - pub fn set_material_texture( - &mut self, - material_name: &str, - diffuse_texture: Texture, - normal_texture: Texture, - bind_group: wgpu::BindGroup, - texture_tag: Option<String>, - ) -> bool { - if let Some(material) = self - .materials - .iter_mut() - .find(|mat| mat.name == material_name) - { - material.diffuse_texture = diffuse_texture; - material.normal_texture = normal_texture; - material.bind_group = bind_group; - material.texture_tag = texture_tag; - true - } else { - false - } - } +impl GLTFTextureInformation { + fn fetch(tex: &gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation { + let sampler = tex.sampler(); - /// Removes any stored texture tag for the supplied material. - pub fn clear_material_texture_tag(&mut self, material_name: &str) -> bool { - if let Some(material) = self - .materials - .iter_mut() - .find(|mat| mat.name == material_name) - { - material.texture_tag = None; - true - } else { - false + 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, + } } - } - /// Returns `true` if a material with `material_name` exists within this model. - pub fn contains_material(&self, material_name: &str) -> bool { - self.materials.iter().any(|mat| mat.name == material_name) - } + let sampler = 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, + }; - /// Returns the registered asset handle for this model, if available. - pub fn asset_handle(&self) -> Option<AssetHandle> { - self.asset_handle_raw(&ASSET_REGISTRY) - } + let image_index = tex.source().index(); + let image_data = &images[image_index]; - pub fn asset_handle_raw(&self, registry: &AssetRegistry) -> Option<AssetHandle> { - registry.model_handle_from_reference(&self.path) - } + let width = image_data.width; + let height = image_data.height; - /// Returns `true` if this model was loaded from the specified resource reference. - pub fn matches_resource(&self, reference: &ResourceReference) -> bool { - &self.path == reference - } + let mip_level_count = (width.max(height) as f32).log2().floor() as u32 + 1; - /// Returns `true` if this model owns the supplied material handle. - pub fn contains_material_handle(&self, material_handle: AssetHandle) -> bool { - self.contains_material_handle_raw(&ASSET_REGISTRY, material_handle) - } + let (pixels, format) = match image_data.format { + Format::R8 => (image_data.pixels.clone(), wgpu::TextureFormat::R8Unorm), + Format::R8G8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rg8Unorm), + Format::R8G8B8 => { + let mut rgba = Vec::with_capacity(image_data.pixels.len() / 3 * 4); + for chunk in image_data.pixels.chunks(3) { + rgba.extend_from_slice(chunk); + rgba.push(255); + } + (rgba, wgpu::TextureFormat::Rgba8Unorm) + }, + Format::R8G8B8A8 => (image_data.pixels.clone(), wgpu::TextureFormat::Rgba8Unorm), + _ => panic!("Unsupported format"), + }; - pub fn contains_material_handle_raw( - &self, - registry: &AssetRegistry, - material_handle: AssetHandle, - ) -> bool { - registry.material_owner(material_handle) == Some(self.id) + GLTFTextureInformation { + sampler, + mip_level_count, + pixels, + format, + width, + height, + } } +} - /// Returns `true` if this model owns a material registered under the provided resource reference. - pub fn contains_material_reference(&self, reference: &ResourceReference) -> bool { - self.contains_material_reference_raw(&ASSET_REGISTRY, reference) - } +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]>, +} - pub fn contains_material_reference_raw( - &self, - registry: &AssetRegistry, - reference: &ResourceReference, - ) -> bool { - registry - .material_handle_from_reference(reference) - .map_or(false, |handle| { - self.contains_material_handle_raw(registry, handle) - }) - } +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, +} - /// Returns `true` if any material on this model is tagged with `texture_tag`. - pub fn contains_texture_tag(&self, texture_tag: &str) -> bool { - self.materials - .iter() - .any(|mat| mat.texture_tag.as_deref() == Some(texture_tag)) - } +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, +} - /// Returns `true` if the specified material currently carries `texture_tag`. - pub fn material_has_texture_tag(&self, material_name: &str, texture_tag: &str) -> bool { - self.materials - .iter() - .find(|mat| mat.name == material_name) - .and_then(|mat| mat.texture_tag.as_deref()) - == Some(texture_tag) - } +impl Model { + fn load_materials( + gltf: &gltf::Document, + _buffers: &Vec<gltf::buffer::Data>, + images: &Vec<gltf::image::Data>, + ) -> Vec<GLTFMaterialInformation> { + 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)) + }; - pub async fn load_from_memory<B>( - graphics: Arc<SharedGraphicsContext>, - buffer: B, - label: Option<&str>, - import_scale: Option<f64>, - ) -> anyhow::Result<LoadedModel> - where - B: AsRef<[u8]>, - { - Self::load_from_memory_raw( - graphics, - buffer, - label, - &ASSET_REGISTRY, - LazyLock::force(&MODEL_CACHE), - import_scale - ) - .await - } + let mut material_data = Vec::new(); - pub async fn load_from_memory_raw<B>( - graphics: Arc<SharedGraphicsContext>, - buffer: B, - label: Option<&str>, - registry: &AssetRegistry, - cache: &Mutex<HashMap<String, Arc<Model>>>, - import_scale: Option<f64>, - ) -> anyhow::Result<LoadedModel> - where - B: AsRef<[u8]>, - { - let start = Instant::now(); - let mut hasher = DefaultHasher::new(); - - let scale_key = import_scale.unwrap_or(1.0); - let cache_key = format!( - "{}::import_scale={:.8}", - label.unwrap_or("default"), - scale_key - ); + for material in gltf.materials() { + let material_name = material.name().unwrap_or("Unnamed Material").to_string(); + puffin::profile_scope!("loading material", &material_name); - if let Some(cached_model) = { - let cache_guard = cache.lock(); - cache_guard.get(&cache_key).cloned() - } { - log::debug!("Model loaded from memory cache: {:?}", cache_key); - return Ok(LoadedModel::new_raw(registry, cached_model)); + let tint = material.pbr_metallic_roughness().base_color_factor(); + let tint = [tint[0], tint[1], tint[2], tint[3]]; + + let pbr = material.pbr_metallic_roughness(); + let diffuse_texture = pbr.base_color_texture(); + let metallic_roughness_texture = pbr.metallic_roughness_texture(); + let normal_texture = material.normal_texture(); + let occlusion_texture = material.occlusion_texture(); + let emissive_texture = material.emissive_texture(); + + let diffuse_texture_info = diffuse_texture + .as_ref() + .and_then(|info| process_texture(info.texture())); + let metallic_roughness_texture_info = metallic_roughness_texture + .as_ref() + .and_then(|info| process_texture(info.texture())); + + let normal_texture_info = normal_texture + .as_ref() + .and_then(|info| process_texture(info.texture())); + let occlusion_texture_info = occlusion_texture + .as_ref() + .and_then(|info| process_texture(info.texture())); + let emissive_texture_info = emissive_texture + .as_ref() + .and_then(|info| process_texture(info.texture())); + + let emissive_factor = material.emissive_factor(); + let emissive_factor = [ + emissive_factor[0], + emissive_factor[1], + emissive_factor[2], + ]; + let metallic_factor = pbr.metallic_factor(); + let roughness_factor = pbr.roughness_factor(); + let alpha_mode = material.alpha_mode(); + let alpha_cutoff = material.alpha_cutoff(); + let double_sided = material.double_sided(); + let occlusion_strength = occlusion_texture + .as_ref() + .map(|info| info.strength()) + .unwrap_or(1.0); + let normal_scale = normal_texture + .as_ref() + .map(|info| info.scale()) + .unwrap_or(1.0); + + material_data.push(GLTFMaterialInformation { + name: material_name, + 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, + roughness_factor, + alpha_mode, + alpha_cutoff, + double_sided, + occlusion_strength, + normal_scale, + }); } - log::trace!( - "========== Benchmarking speed of loading {:?} ==========", - label - ); - log::debug!("Loading from memory"); - let res_ref = ResourceReference::from_bytes(buffer.as_ref()); + if material_data.is_empty() { + material_data.push(GLTFMaterialInformation { + name: "Default".to_string(), + 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, + roughness_factor: 1.0, + alpha_mode: gltf::material::AlphaMode::Opaque, + alpha_cutoff: None, + double_sided: false, + occlusion_strength: 1.0, + normal_scale: 1.0, + }); + } - let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?; - let mut meshes = Vec::new(); + material_data + } - let mut texture_data: Vec<(String, Option<Vec<u8>>, Option<Vec<u8>>, [f32; 4])> = Vec::new(); - for material in gltf.materials() { - log::debug!("Processing material: {:?}", material.name()); - let material_name = material.name().unwrap_or("Unnamed Material").to_string(); + fn load_meshes( + mesh: &gltf::Mesh, + buffers: &Vec<gltf::buffer::Data>, + 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 + .read_positions() + .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))? + .collect(); + + let indices: Vec<u32> = reader + .read_indices() + .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))? + .into_u32() + .collect(); + + let normals: Vec<[f32; 3]> = reader + .read_normals() + .map(|iter| iter.collect()) + .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]); + + let tangents: Vec<[f32; 4]> = reader + .read_tangents() + .map(|iter| iter.collect()) + .unwrap_or_else(|| vec![[0.0, 0.0, 0.0, 1.0]; positions.len()]); + + let colors: Vec<[f32; 4]> = reader + .read_colors(0) + .map(|iter| iter.into_rgba_f32().collect()) + .unwrap_or_else(|| vec![[1.0; 4]; positions.len()]); + + let joints: Vec<[u16; 4]> = reader + .read_joints(0) + .map(|iter| iter.into_u16().collect()) + .unwrap_or_else(|| vec![[0u16; 4]; positions.len()]); + + let mut weights: Vec<[f32; 4]> = reader + .read_weights(0) + .map(|iter| iter.into_f32().collect()) + .unwrap_or_else(|| vec![[1.0, 0.0, 0.0, 0.0]; positions.len()]); + + for weight in &mut weights { + let sum = weight[0] + weight[1] + weight[2] + weight[3]; + if sum > 0.0 { + weight[0] /= sum; + weight[1] /= sum; + weight[2] /= sum; + weight[3] /= sum; + } + } + + let tex_coords: Vec<[f32; 2]> = reader + .read_tex_coords(0) + .map(|iter| iter.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); + + let tex_coords1: Vec<[f32; 2]> = reader + .read_tex_coords(1) + .map(|iter| iter.into_f32().collect()) + .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); + + let expected_len = positions.len(); + let check_len = |label: &str, len: usize| -> anyhow::Result<()> { + if len == expected_len { + Ok(()) + } else { + Err(anyhow::anyhow!( + "Mesh attribute length mismatch for {}: expected {}, got {}", + label, + expected_len, + len + )) + } + }; - let tint = material - .pbr_metallic_roughness() - .base_color_factor(); + check_len("normals", normals.len())?; + check_len("tangents", tangents.len())?; + check_len("colors", colors.len())?; + check_len("joints", joints.len())?; + check_len("weights", weights.len())?; + check_len("tex_coords0", tex_coords.len())?; + check_len("tex_coords1", tex_coords1.len())?; + + mesh_collector.push(GLTFMeshInformation { + name: mesh_name.clone(), + primitive_index, + material_index: primitive.material().index().unwrap_or(0), + mode: primitive.mode(), + positions, + indices, + normals, + tangents, + colors, + joints, + weights, + tex_coords0: tex_coords, + tex_coords1, + }); + } - let tint = [tint[0], tint[1], tint[2], tint[3]]; + Ok(()) + } - let diffuse_bytes = if let Some(pbr) = material.pbr_metallic_roughness().base_color_texture() { - let texture_info = pbr.texture(); - let image = texture_info.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 + 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 { - None + 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 + } - let normal_bytes = if let Some(info) = material.normal_texture() { - let image = info.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()) + 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::image::Source::Uri { uri, mime_type: _ } => { - log::warn!("External URI textures not supported: {}", uri); - None + 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>>, + ) -> 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 = { + puffin::profile_scope!("hashing model"); + let mut hasher = DefaultHasher::default(); + if let Some(label) = label { + label.hash(&mut hasher); } else { - None + buffer.as_ref().hash(&mut hasher); }; + hasher.finish() + }; - texture_data.push((material_name, diffuse_bytes, normal_bytes, tint)); + if let Some(model) = registry.model_handle_by_hash(hash) { + return Ok(model); } - if texture_data.is_empty() { - texture_data.push(( - "Default".to_string(), - None, - None, - [1.0, 1.0, 1.0, 1.0], - )); - } + let (gltf, buffers, images) = gltf::import_slice(buffer.as_ref())?; - let parallel_start = Instant::now(); - let processed_textures: Vec<_> = texture_data - .into_par_iter() - .map(|(material_name, diffuse_bytes, normal_bytes, tint)| { - let material_start = Instant::now(); - - let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| { - let load_start = Instant::now(); - let image = match image::load_from_memory(bytes) { - Ok(image) => image, - Err(err) => { - log::warn!( - "Failed to decode diffuse texture for material '{}': {} ({} bytes)", - material_name, - err, - bytes.len() - ); - return None; - } - }; - log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed()); - - let rgba_start = Instant::now(); - let rgba = image.to_rgba8(); - log::trace!( - "Converting diffuse image to rgba8 took {:?}", - rgba_start.elapsed() - ); - - let dimensions = image.dimensions(); - Some((rgba.into_raw(), dimensions)) - }); + let mut meshes = Vec::new(); + for mesh in gltf.meshes() { + Self::load_meshes(&mesh, &buffers, &mut meshes)?; + } - let processed_normal = normal_bytes.as_ref().and_then(|bytes| { - let load_start = Instant::now(); - let image = match image::load_from_memory(bytes) { - Ok(image) => image, - Err(err) => { - log::warn!( - "Failed to decode normal texture for material '{}': {} ({} bytes)", - material_name, - err, - bytes.len() - ); - return None; - } - }; - log::trace!("Loading normal image to memory: {:?}", load_start.elapsed()); - - let rgba_start = Instant::now(); - let rgba = image.to_rgba8(); - log::trace!( - "Converting normal image to rgba8 took {:?}", - rgba_start.elapsed() - ); - - let dimensions = image.dimensions(); - Some((rgba.into_raw(), dimensions)) - }); + 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 + ); - log::trace!( - "Parallel processing of material '{}' took: {:?}", - material_name, - material_start.elapsed() - ); + let material_data = Self::load_materials(&gltf, &buffers, &images); - (material_name, processed_diffuse, processed_normal, tint) + let processed_textures: Vec<ProcessedMaterialTextures> = material_data + .into_par_iter() + .map(|material_info| { + puffin::profile_scope!("processing material textures"); + let material_name = material_info.name; + + 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; + let roughness_factor = material_info.roughness_factor; + let alpha_mode = material_info.alpha_mode; + let alpha_cutoff = material_info.alpha_cutoff; + let double_sided = material_info.double_sided; + let occlusion_strength = material_info.occlusion_strength; + let normal_scale = material_info.normal_scale; + + ProcessedMaterialTextures { + name: material_name, + diffuse: processed_diffuse, + normal: processed_normal, + emissive: processed_emissive, + metallic_roughness: processed_metallic_roughness, + occlusion: processed_occlusion, + diffuse_sampler, + normal_sampler, + emissive_sampler, + metallic_roughness_sampler, + occlusion_sampler, + tint, + emissive_factor, + metallic_factor, + roughness_factor, + alpha_mode, + alpha_cutoff, + double_sided, + occlusion_strength, + normal_scale, + } }) .collect(); - log::trace!( - "Total parallel image processing took: {:?}", - parallel_start.elapsed() - ); - let mut materials = Vec::new(); let grey_texture = registry.grey_texture(graphics.clone()); let flat_normal_texture = registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); - for (material_name, processed_diffuse, processed_normal, tint) in processed_textures { - let start = Instant::now(); + for processed in processed_textures { + puffin::profile_scope!("creating material"); + + let material_name = processed.name; + let processed_diffuse = processed.diffuse; + let processed_normal = processed.normal; + let processed_emissive = processed.emissive; + let processed_metallic_roughness = processed.metallic_roughness; + let processed_occlusion = processed.occlusion; + let diffuse_sampler = processed.diffuse_sampler; + let normal_sampler = processed.normal_sampler; + let emissive_sampler = processed.emissive_sampler; + let metallic_roughness_sampler = processed.metallic_roughness_sampler; + let occlusion_sampler = processed.occlusion_sampler; let diffuse_texture = if let Some((rgba_data, dimensions)) = processed_diffuse { Texture::from_bytes_verbose_mipmapped( @@ -625,11 +945,13 @@ impl Model { &rgba_data, Some(dimensions), None, - None, + diffuse_sampler.clone(), Some(material_name.as_str()) ) + } else if let Some(grey) = registry.get_texture(grey_texture) { + (*grey).clone() } else { - (*grey_texture).clone() + anyhow::bail!("Unable to find processed diffuse or fetch fallback texture for model {:?}", label); }; let normal_texture = if let Some((rgba_data, dimensions)) = processed_normal { @@ -638,269 +960,147 @@ impl Model { &rgba_data, Some(dimensions), None, - None, + normal_sampler.clone(), Some(material_name.as_str()) ) + } else if let Some(tex) = registry.get_texture(flat_normal_texture) { + (*tex).clone() } else { - (*flat_normal_texture).clone() + anyhow::bail!("Unable to find processed normal or fetch fallback texture for model {:?}", label); }; + + let emissive_texture = processed_emissive.map(|(rgba_data, dimensions)| { + Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &rgba_data, + Some(dimensions), + None, + emissive_sampler.clone(), + Some(material_name.as_str()) + ) + }); + let metallic_roughness_texture = + processed_metallic_roughness.map(|(rgba_data, dimensions)| { + Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &rgba_data, + Some(dimensions), + None, + metallic_roughness_sampler.clone(), + Some(material_name.as_str()) + ) + }); + let occlusion_texture = processed_occlusion.map(|(rgba_data, dimensions)| { + Texture::from_bytes_verbose_mipmapped( + graphics.clone(), + &rgba_data, + Some(dimensions), + None, + occlusion_sampler.clone(), + Some(material_name.as_str()) + ) + }); let texture_tag = Some(material_name.clone()); - materials.push(Material::new_with_tint( + let mut material = Material::new( graphics.clone(), material_name, diffuse_texture, normal_texture, - tint, + processed.tint, texture_tag, - )); + ); - log::trace!("Time to create GPU texture: {:?}", start.elapsed()); + material.emissive_factor = processed.emissive_factor; + material.metallic_factor = processed.metallic_factor; + material.roughness_factor = processed.roughness_factor; + material.alpha_mode = processed.alpha_mode; + material.alpha_cutoff = processed.alpha_cutoff; + material.double_sided = processed.double_sided; + material.occlusion_strength = processed.occlusion_strength; + material.normal_scale = processed.normal_scale; + material.emissive_texture = emissive_texture; + material.metallic_roughness_texture = metallic_roughness_texture; + material.occlusion_texture = occlusion_texture; + material.sync_uniform(&graphics); + + materials.push(material); } - for mesh in gltf.meshes() { - log::debug!("Processing mesh: {:?}", mesh.name()); - for primitive in mesh.primitives() { - let reader = primitive.reader(|buffer| Some(&buffers[buffer.index()])); - - let positions: Vec<[f32; 3]> = reader - .read_positions() - .ok_or_else(|| anyhow::anyhow!("Mesh missing positions"))? - .collect(); - - let normals: Vec<[f32; 3]> = reader - .read_normals() - .map(|iter| iter.collect()) - .unwrap_or_else(|| vec![[0.0, 1.0, 0.0]; positions.len()]); - - let tex_coords: Vec<[f32; 2]> = reader - .read_tex_coords(0) - .map(|iter| iter.into_f32().collect()) - .unwrap_or_else(|| vec![[0.0, 0.0]; positions.len()]); - - let mut vertices: Vec<ModelVertex> = positions - .iter() - .zip(normals.iter()) - .zip(tex_coords.iter()) - .map(|((pos, norm), tex)| ModelVertex { - position: *pos, - normal: *norm, - tex_coords: *tex, - tangent: [0.0; 3], - bitangent: [0.0; 3], - }) - .collect(); - - // Apply import scaling at load time so downstream systems (bounds, physics, etc.) - // see the baked geometry rather than a render-time transform hack. - 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; - } - } - - for v in &vertices { - let _ = v.position.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = v.normal.iter().map(|v| (*v as i32).hash(&mut hasher)); - let _ = v.tex_coords.iter().map(|v| (*v as i32).hash(&mut hasher)); - } - - let indices: Vec<u32> = reader - .read_indices() - .ok_or_else(|| anyhow::anyhow!("Mesh missing indices"))? - .into_u32() - .collect(); - indices.hash(&mut hasher); - - let mut triangles_included = vec![0; vertices.len()]; - for c in indices.chunks(3) { - let v0 = vertices[c[0] as usize]; - let v1 = vertices[c[1] as usize]; - let v2 = vertices[c[2] as usize]; - - let pos0: Vec3 = v0.position.into(); - let pos1: Vec3 = v1.position.into(); - let pos2: Vec3 = v2.position.into(); - - let uv0: Vec2 = v0.tex_coords.into(); - let uv1: Vec2 = v1.tex_coords.into(); - let uv2: Vec2 = v2.tex_coords.into(); - - // Calculate the edges of the triangle - let delta_pos1 = pos1 - pos0; - let delta_pos2 = pos2 - pos0; - - // This will give us a direction to calculate the - // tangent and bitangent - let delta_uv1 = uv1 - uv0; - let delta_uv2 = uv2 - uv0; - - // Solving the following system of equations will - // give us the tangent and bitangent. - // delta_pos1 = delta_uv1.x * T + delta_u.y * B - // delta_pos2 = delta_uv2.x * T + delta_uv2.y * B - // Luckily, the place I found this equation provided - // the solution! - let r = 1.0 / (delta_uv1.x * delta_uv2.y - delta_uv1.y * delta_uv2.x); - let tangent = (delta_pos1 * delta_uv2.y - delta_pos2 * delta_uv1.y) * r; - // We flip the bitangent to enable right-handed normal - // maps with wgpu texture coordinate system - let bitangent = (delta_pos2 * delta_uv1.x - delta_pos1 * delta_uv2.x) * -r; - - // We'll use the same tangent/bitangent for each vertex in the triangle - vertices[c[0] as usize].tangent = - (tangent + Vec3::from(vertices[c[0] as usize].tangent)).into(); - vertices[c[1] as usize].tangent = - (tangent + Vec3::from(vertices[c[1] as usize].tangent)).into(); - vertices[c[2] as usize].tangent = - (tangent + Vec3::from(vertices[c[2] as usize].tangent)).into(); - vertices[c[0] as usize].bitangent = - (bitangent + Vec3::from(vertices[c[0] as usize].bitangent)).into(); - vertices[c[1] as usize].bitangent = - (bitangent + Vec3::from(vertices[c[1] as usize].bitangent)).into(); - vertices[c[2] as usize].bitangent = - (bitangent + Vec3::from(vertices[c[2] as usize].bitangent)).into(); - - // Used to average the tangents/bitangents - triangles_included[c[0] as usize] += 1; - triangles_included[c[1] as usize] += 1; - triangles_included[c[2] as usize] += 1; - } - - for (i, n) in triangles_included.into_iter().enumerate() { - let denom = 1.0 / n as f32; - let v = &mut vertices[i]; - v.tangent = (Vec3::from(v.tangent) * denom).into(); - v.bitangent = (Vec3::from(v.bitangent) * denom).into(); - } + let mut gpu_meshes = Vec::new(); + for mesh_info in meshes { + if mesh_info.mode != gltf::mesh::Mode::Triangles { + return Err(anyhow::anyhow!( + "Unsupported primitive mode {:?} for mesh '{}' (primitive {})", + mesh_info.mode, + mesh_info.name, + mesh_info.primitive_index + )); + } - let vertex_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Vertex Buffer", label)), - contents: bytemuck::cast_slice(&vertices), - usage: wgpu::BufferUsages::VERTEX, - }); - - let index_buffer = - graphics - .device - .create_buffer_init(&wgpu::util::BufferInitDescriptor { - label: Some(&format!("{:?} Index Buffer", label)), - contents: bytemuck::cast_slice(&indices), - usage: wgpu::BufferUsages::INDEX, - }); - - let material_index = primitive.material().index().unwrap_or(0); - - meshes.push(Mesh { - name: mesh.name().unwrap_or("Unnamed Mesh").to_string(), - vertex_buffer, - index_buffer, - vertices, - num_elements: indices.len() as u32, - material: material_index, + let mut vertices = Vec::with_capacity(mesh_info.positions.len()); + for index in 0..mesh_info.positions.len() { + vertices.push(ModelVertex { + position: mesh_info.positions[index], + normal: mesh_info.normals[index], + tangent: mesh_info.tangents[index], + tex_coords0: mesh_info.tex_coords0[index], + tex_coords1: mesh_info.tex_coords1[index], + colour0: mesh_info.colors[index], + joints0: mesh_info.joints[index], + weights0: mesh_info.weights[index], }); } + + let vertex_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Vertex Buffer", model_label)), + contents: bytemuck::cast_slice(&vertices), + usage: wgpu::BufferUsages::VERTEX, + }); + + let index_buffer = + graphics + .device + .create_buffer_init(&wgpu::util::BufferInitDescriptor { + label: Some(&format!("{} Index Buffer", model_label)), + contents: bytemuck::cast_slice(&mesh_info.indices), + usage: wgpu::BufferUsages::INDEX, + }); + + gpu_meshes.push(Mesh { + name: mesh_info.name, + vertex_buffer, + index_buffer, + vertices, + num_elements: mesh_info.indices.len() as u32, + material: mesh_info.material_index, + }); } log::debug!("Successfully loaded model [{:?}]", label); - let model = Arc::new(Model { - meshes, + let model = Model { + label: model_label.to_string(), + hash, + path: ResourceReference::from_bytes(buffer.as_ref()), + meshes: gpu_meshes, materials, - label: label.unwrap_or("No named model").to_string(), - path: res_ref, - id: ModelId(hasher.finish()), - }); - - let loaded = LoadedModel::new_raw(registry, Arc::clone(&model)); - - { - let mut cache_guard = cache.lock(); - cache_guard.insert(cache_key.clone(), model); - } - log::trace!("==================== DONE ===================="); - log::debug!("Model cached from memory: {:?}", label); - log::debug!("Took {:?} to load model: {:?}", start.elapsed(), label); - log::trace!("=============================================="); - Ok(loaded) - } - - pub async fn load( - graphics: Arc<SharedGraphicsContext>, - path: &PathBuf, - label: Option<&str>, - import_scale: Option<f64>, - ) -> anyhow::Result<LoadedModel> { - Self::load_raw( - graphics, - path, - label, - &ASSET_REGISTRY, - LazyLock::force(&MODEL_CACHE), - import_scale - ) - .await - } - - pub async fn load_raw( - graphics: Arc<SharedGraphicsContext>, - path: &PathBuf, - label: Option<&str>, - registry: &AssetRegistry, - cache: &Mutex<HashMap<String, Arc<Model>>>, - import_scale: Option<f64>, - ) -> anyhow::Result<LoadedModel> { - let file_name = path.file_name(); - log::debug!("Loading model [{:?}]", file_name); - - let scale_key = import_scale.unwrap_or(1.0); - let path_str = format!("{}::import_scale={:.8}", path.to_string_lossy(), scale_key); - - log::debug!("Checking if model exists in cache"); - if let Some(cached_model) = { - let cache_guard = cache.lock(); - cache_guard.get(&path_str).cloned() - } { - log::debug!("Model loaded from cache: {:?}", path_str); - return Ok(LoadedModel::new_raw(registry, cached_model)); - } - log::debug!("Model does not exist in cache, loading memory..."); - - log::debug!("Path of model: {}", path.display()); - - let buffer = std::fs::read(path)?; - let loaded = - Self::load_from_memory_raw(graphics, buffer, label, registry, cache, import_scale) - .await?; - - let mut model_clone: Model = (*loaded).clone(); - if let Ok(reference) = ResourceReference::from_path(path) { - model_clone.path = reference; - } - if let Some(custom_label) = label { - model_clone.label = custom_label.to_string(); - } + skins, + animations, + nodes, + }; - let updated = Arc::new(model_clone); - { - let mut cache_guard = cache.lock(); - cache_guard.insert(path_str.clone(), Arc::clone(&updated)); - if let Some(custom_label) = label { - let label_key = format!("{}::import_scale={:.8}", custom_label, scale_key); - cache_guard.insert(label_key, Arc::clone(&updated)); - } - } + let handle = if let Some(label) = label { + registry.add_model_with_label(label, model) + } else { + registry.add_model(model) + }; - log::debug!("Model cached and loaded: {:?}", file_name); - Ok(LoadedModel::new_raw(registry, updated)) + Ok(handle) } - } pub trait DrawModel<'a> { @@ -911,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, @@ -919,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)] @@ -947,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( @@ -958,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); } @@ -992,6 +1200,7 @@ where instances.clone(), camera_bind_group, light_bind_group, + None, // Provide an AnimationComponent if available in a future update ); } } @@ -1083,50 +1292,6 @@ where } } -pub trait DrawShadow<'a> { - fn draw_shadow_mesh_instanced( - &mut self, - mesh: &'a Mesh, - instances: Range<u32>, - light_bind_group: &'a wgpu::BindGroup, - ); - - fn draw_shadow_model_instanced( - &mut self, - model: &'a Model, - instances: Range<u32>, - light_bind_group: &'a wgpu::BindGroup, - ); -} - -impl<'a, 'b> DrawShadow<'b> for wgpu::RenderPass<'a> -where - 'b: 'a, -{ - fn draw_shadow_mesh_instanced( - &mut self, - mesh: &'b Mesh, - instances: Range<u32>, - light_bind_group: &'b 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, light_bind_group, &[]); - self.draw_indexed(0..mesh.num_elements, 0, instances); - } - - fn draw_shadow_model_instanced( - &mut self, - model: &'b Model, - instances: Range<u32>, - light_bind_group: &'b wgpu::BindGroup, - ) { - for mesh in &model.meshes { - self.draw_shadow_mesh_instanced(mesh, instances.clone(), light_bind_group); - } - } -} - pub trait Vertex { fn desc() -> VertexBufferLayout<'static>; } @@ -1135,20 +1300,26 @@ pub trait Vertex { /// ```wgsl /// struct VertexInput { /// @location(0) position: vec3<f32>, -/// @location(1) tex_coords: vec2<f32>, -/// @location(2) normal: vec3<f32>, -/// @location(3) tangent: vec3<f32>, -/// @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) color0: vec4<f32>, +/// @location(6) joints0: vec4<u32>, +/// @location(7) weights0: vec4<f32>, /// }; /// ``` #[repr(C)] -#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable, serde::Serialize, serde::Deserialize)] +#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct ModelVertex { pub position: [f32; 3], - pub tex_coords: [f32; 2], pub normal: [f32; 3], - pub tangent: [f32; 3], - pub bitangent: [f32; 3], + pub tangent: [f32; 4], // xyz + handedness (w) + pub tex_coords0: [f32; 2], + pub tex_coords1: [f32; 2], // optional, can be zeroed if missing + pub colour0: [f32; 4], // optional, default to white + pub joints0: [u16; 4], + pub weights0: [f32; 4], } impl Vertex for ModelVertex { @@ -1157,54 +1328,71 @@ impl Vertex for ModelVertex { array_stride: mem::size_of::<ModelVertex>() as BufferAddress, step_mode: wgpu::VertexStepMode::Vertex, attributes: &[ + // position VertexAttribute { format: wgpu::VertexFormat::Float32x3, offset: 0, shader_location: 0, }, + // normal wgpu::VertexAttribute { offset: mem::size_of::<[f32; 3]>() as wgpu::BufferAddress, shader_location: 1, - format: wgpu::VertexFormat::Float32x2, + format: wgpu::VertexFormat::Float32x3, }, + // tangent wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 5]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 6]>() as wgpu::BufferAddress, shader_location: 2, - format: wgpu::VertexFormat::Float32x3, + format: wgpu::VertexFormat::Float32x4, }, + // tex_coords0 wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 8]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 10]>() as wgpu::BufferAddress, shader_location: 3, - format: wgpu::VertexFormat::Float32x3, + format: wgpu::VertexFormat::Float32x2, }, + // tex_coords1 wgpu::VertexAttribute { - offset: mem::size_of::<[f32; 11]>() as wgpu::BufferAddress, + offset: mem::size_of::<[f32; 12]>() as wgpu::BufferAddress, shader_location: 4, - format: wgpu::VertexFormat::Float32x3, + format: wgpu::VertexFormat::Float32x2, + }, + // color0 + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 14]>() as wgpu::BufferAddress, + shader_location: 5, + format: wgpu::VertexFormat::Float32x4, + }, + // joints0 + wgpu::VertexAttribute { + offset: mem::size_of::<[f32; 18]>() as wgpu::BufferAddress, + shader_location: 6, + format: wgpu::VertexFormat::Uint16x4, + }, + // weights0 + wgpu::VertexAttribute { + offset: (mem::size_of::<[f32; 18]>() + mem::size_of::<[u16; 4]>()) + as wgpu::BufferAddress, + shader_location: 7, + format: wgpu::VertexFormat::Float32x4, }, ], } } } -#[repr(C, align(16))] +#[repr(C)] #[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)] pub struct MaterialUniform { - /// RGBA tint multiplier applied to the sampled base colour. - pub colour: [f32; 4], - - /// Scales incoming UVs before sampling (repeat counts when using Repeat wrap mode). + pub base_colour: [f32; 4], + pub emissive: [f32; 3], + pub emissive_strength: f32, + pub metallic: f32, + pub roughness: f32, + pub normal_scale: f32, + pub occlusion_strength: f32, + pub alpha_cutoff: f32, pub uv_tiling: [f32; 2], - - pub _pad: [f32; 2], -} - -impl MaterialUniform { - pub fn new(colour: [f32; 4]) -> Self { - Self { - colour, - uv_tiling: [1.0, 1.0], - _pad: [0.0, 0.0], - } - } + pub _pad: f32, } @@ -10,7 +10,6 @@ use crate::lighting::{Light, LightArrayUniform, LightComponent, MAX_LIGHTS}; use crate::model::Vertex; use crate::pipelines::DropbearShaderPipeline; use crate::shader::Shader; -use crate::texture::Texture; pub struct LightCubePipeline { shader: Shader, @@ -94,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", @@ -231,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,48 +5,62 @@ 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; let uv_y = 1.0_f32; let uv_z = 1.0_f32; + let make_vertex = |position: [f32; 3], normal: [f32; 3], tangent: [f32; 3], uv: [f32; 2]| { + ModelVertex { + position, + normal, + tangent: [tangent[0], tangent[1], tangent[2], 1.0], + tex_coords0: uv, + tex_coords1: [0.0, 0.0], + colour0: [1.0, 1.0, 1.0, 1.0], + joints0: [0, 0, 0, 0], + weights0: [1.0, 0.0, 0.0, 0.0], + } + }; + let vertices = vec![ // Front Face (Normal: 0, 0, 1) - ModelVertex { position: [-half.x, -half.y, half.z], tex_coords: [0.0, uv_y], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [ half.x, -half.y, half.z], tex_coords: [uv_x, uv_y], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [ half.x, half.y, half.z], tex_coords: [uv_x, 0.0], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [-half.x, half.y, half.z], tex_coords: [0.0, 0.0], normal: [0.0, 0.0, 1.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, + make_vertex([-half.x, -half.y, half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, uv_y]), + make_vertex([ half.x, -half.y, half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [uv_x, uv_y]), + make_vertex([ half.x, half.y, half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [uv_x, 0.0]), + make_vertex([-half.x, half.y, half.z], [0.0, 0.0, 1.0], [1.0, 0.0, 0.0], [0.0, 0.0]), // Back Face (Normal: 0, 0, -1) - ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [0.0, uv_y], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [uv_x, uv_y], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [-half.x, half.y, -half.z], tex_coords: [uv_x, 0.0], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [ half.x, half.y, -half.z], tex_coords: [0.0, 0.0], normal: [0.0, 0.0, -1.0], tangent: [-1.0, 0.0, 0.0], bitangent: [0.0, -1.0, 0.0] }, + make_vertex([ half.x, -half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0, uv_y]), + make_vertex([-half.x, -half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [uv_x, uv_y]), + make_vertex([-half.x, half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [uv_x, 0.0]), + make_vertex([ half.x, half.y, -half.z], [0.0, 0.0, -1.0], [-1.0, 0.0, 0.0], [0.0, 0.0]), // Top Face (Normal: 0, 1, 0) - ModelVertex { position: [-half.x, half.y, half.z], tex_coords: [0.0, uv_z], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] }, - ModelVertex { position: [ half.x, half.y, half.z], tex_coords: [uv_x, uv_z], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] }, - ModelVertex { position: [ half.x, half.y, -half.z], tex_coords: [uv_x, 0.0], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] }, - ModelVertex { position: [-half.x, half.y, -half.z], tex_coords: [0.0, 0.0], normal: [0.0, 1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, 1.0] }, + make_vertex([-half.x, half.y, half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, uv_z]), + make_vertex([ half.x, half.y, half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, uv_z]), + make_vertex([ half.x, half.y, -half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, 0.0]), + make_vertex([-half.x, half.y, -half.z], [0.0, 1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0]), // Bottom Face (Normal: 0, -1, 0) - ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [0.0, uv_z], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] }, - ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [uv_x, uv_z], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] }, - ModelVertex { position: [ half.x, -half.y, half.z], tex_coords: [uv_x, 0.0], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] }, - ModelVertex { position: [-half.x, -half.y, half.z], tex_coords: [0.0, 0.0], normal: [0.0, -1.0, 0.0], tangent: [1.0, 0.0, 0.0], bitangent: [0.0, 0.0, -1.0] }, + make_vertex([-half.x, -half.y, -half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, uv_z]), + make_vertex([ half.x, -half.y, -half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, uv_z]), + make_vertex([ half.x, -half.y, half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [uv_x, 0.0]), + make_vertex([-half.x, -half.y, half.z], [0.0, -1.0, 0.0], [1.0, 0.0, 0.0], [0.0, 0.0]), // Right Face (Normal: 1, 0, 0) - ModelVertex { position: [ half.x, -half.y, half.z], tex_coords: [0.0, uv_y], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [ half.x, -half.y, -half.z], tex_coords: [uv_z, uv_y], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [ half.x, half.y, -half.z], tex_coords: [uv_z, 0.0], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [ half.x, half.y, half.z], tex_coords: [0.0, 0.0], normal: [1.0, 0.0, 0.0], tangent: [0.0, 0.0, -1.0], bitangent: [0.0, -1.0, 0.0] }, + make_vertex([ half.x, -half.y, half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, uv_y]), + make_vertex([ half.x, -half.y, -half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [uv_z, uv_y]), + make_vertex([ half.x, half.y, -half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [uv_z, 0.0]), + make_vertex([ half.x, half.y, half.z], [1.0, 0.0, 0.0], [0.0, 0.0, -1.0], [0.0, 0.0]), // Left Face (Normal: -1, 0, 0) - ModelVertex { position: [-half.x, -half.y, -half.z], tex_coords: [0.0, uv_y], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [-half.x, -half.y, half.z], tex_coords: [uv_z, uv_y], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [-half.x, half.y, half.z], tex_coords: [uv_z, 0.0], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] }, - ModelVertex { position: [-half.x, half.y, -half.z], tex_coords: [0.0, 0.0], normal: [-1.0, 0.0, 0.0], tangent: [0.0, 0.0, 1.0], bitangent: [0.0, -1.0, 0.0] }, + make_vertex([-half.x, -half.y, -half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, uv_y]), + make_vertex([-half.x, -half.y, half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [uv_z, uv_y]), + make_vertex([-half.x, half.y, half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [uv_z, 0.0]), + make_vertex([-half.x, half.y, -half.z], [-1.0, 0.0, 0.0], [0.0, 0.0, 1.0], [0.0, 0.0]), ]; let indices: Vec<u32> = vec![ @@ -1,15 +1,14 @@ //! Starter objects like planes and primitive objects are that made during runtime with //! vertices rather than from a model. -use crate::asset::{AssetRegistry, ASSET_REGISTRY}; +use crate::asset::{AssetRegistry, Handle}; use crate::graphics::SharedGraphicsContext; -use crate::model::{LoadedModel, Material, Mesh, Model, ModelId, MODEL_CACHE}; +use crate::model::{Material, Mesh, Model}; use crate::utils::ResourceReference; use crate::model::ModelVertex; -use parking_lot::Mutex; -use std::collections::HashMap; use std::hash::{DefaultHasher, Hasher}; -use std::sync::{Arc, LazyLock}; +use std::sync::Arc; +use parking_lot::RwLock; use serde::{Deserialize, Serialize}; use wgpu::util::DeviceExt; @@ -45,24 +44,9 @@ impl ProcedurallyGeneratedObject { graphics: Arc<SharedGraphicsContext>, material: Option<Material>, label: Option<&str>, - ) -> LoadedModel { - self.build_model_raw( - graphics, - material, - label, - &ASSET_REGISTRY, - LazyLock::force(&MODEL_CACHE), - ) - } - - pub fn build_model_raw( - self, - graphics: Arc<SharedGraphicsContext>, - material: Option<Material>, - label: Option<&str>, - registry: &AssetRegistry, - cache: &Mutex<HashMap<String, Arc<Model>>>, - ) -> LoadedModel { + 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)); @@ -72,11 +56,10 @@ impl ProcedurallyGeneratedObject { .map(|s| s.to_string()) .unwrap_or_else(|| format!("procedural_{hash:016x}")); - if let Some(cached_model) = { - let cache_guard = cache.lock(); - cache_guard.get(&label).cloned() - } { - return LoadedModel::new_raw(registry, cached_model); + let mut _rguard = registry.write(); + + if let Some(handle) = _rguard.model_handle_by_hash(hash) { + return handle; } let vertices = self.vertices; @@ -108,31 +91,38 @@ impl ProcedurallyGeneratedObject { }; let material = material.unwrap_or_else(|| { - let grey = registry.grey_texture(graphics.clone()); - let flat_normal = registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); - Material::new_with_tint( + let grey_handle = _rguard.grey_texture(graphics.clone()); + let flat_normal_handle = + _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 = _rguard + .get_texture(flat_normal_handle) + .expect("Flat normal texture handle missing") + .clone(); + Material::new( graphics.clone(), "procedural_material", - (*grey).clone(), - (*flat_normal).clone(), + grey, + flat_normal, [1.0, 1.0, 1.0, 1.0], Some("procedural_material".to_string()), ) }); - let model = Arc::new(Model { + let model = Model { label: label.clone(), + hash, path: ResourceReference::from_bytes(hash.to_le_bytes()), meshes: vec![mesh], materials: vec![material], - id: ModelId(hash), - }); - - { - let mut cache_guard = cache.lock(); - cache_guard.insert(label, Arc::clone(&model)); - } + skins: Vec::new(), + animations: Vec::new(), + nodes: Vec::new(), + }; - LoadedModel::new_raw(registry, 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()); @@ -25,10 +25,14 @@ struct CameraUniform { }; struct MaterialUniform { - // for stuff like tinting - float4 colour; - - // scales incoming UVs before sampling + float4 base_colour; + float3 emissive; + float emissive_strength; + float metallic; + float roughness; + float normal_scale; + float occlusion_strength; + float alpha_cutoff; float2 uv_tiling; }; @@ -80,7 +80,7 @@ fn compute_equirect_to_cubemap( let eq_pixel = vec2<i32>(eq_uv * vec2<f32>(textureDimensions(src))); // We use textureLoad() as textureSample() is not allowed in compute shaders - var sample = textureLoad(src, eq_pixel, 0); + var smpl = textureLoad(src, eq_pixel, 0); - textureStore(dst, gid.xy, gid.z, sample); + textureStore(dst, gid.xy, gid.z, smpl); } @@ -23,10 +23,14 @@ struct Light { } struct MaterialUniform { - // for stuff like tinting - colour: vec4<f32>, - - // scales incoming UVs before sampling + base_colour: vec4<f32>, + emissive: vec3<f32>, + emissive_strength: f32, + metallic: f32, + roughness: f32, + normal_scale: f32, + occlusion_strength: f32, + alpha_cutoff: f32, uv_tiling: vec2<f32>, } @@ -40,38 +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(1) tex_coords: vec2<f32>, - @location(2) normal: vec3<f32>, - @location(3) tangent: vec3<f32>, - @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 { @@ -100,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; @@ -203,16 +233,15 @@ 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; } @@ -245,44 +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 view_dir = normalize(u_camera.view_pos.xyz - in.world_position); - - let world_normal = apply_normal_map( - in.world_normal, - in.world_tangent, - in.world_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, @@ -2,7 +2,7 @@ use std::{fs, path::PathBuf, sync::Arc}; use image::GenericImageView; use serde::{Deserialize, Serialize}; - +use crate::asset::AssetRegistry; use crate::graphics::SharedGraphicsContext; use crate::utils::ToPotentialString; @@ -68,6 +68,7 @@ pub struct Texture { pub sampler: wgpu::Sampler, pub size: wgpu::Extent3d, pub view: wgpu::TextureView, + pub(crate) hash: Option<u64>, } impl Texture { @@ -84,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, @@ -109,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, @@ -137,6 +140,7 @@ impl Texture { view, sampler, size, + hash: None, } } @@ -146,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), @@ -184,6 +189,7 @@ impl Texture { size, view, label: label.to_potential_string(), + hash: None, } } @@ -195,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), @@ -222,6 +229,7 @@ impl Texture { sampler, size, view, + hash: None, } } @@ -231,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)) } @@ -239,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) } @@ -253,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, @@ -285,21 +296,26 @@ impl Texture { sampler: Option<wgpu::SamplerDescriptor>, label: Option<&str>, ) -> Self { - 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!( + puffin::profile_function!(label.unwrap_or("")); + let hash = AssetRegistry::hash_bytes(bytes); + + 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, @@ -308,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)) + } } } }; @@ -349,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, @@ -365,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; @@ -416,6 +435,7 @@ impl Texture { sampler, size, view, + hash: Some(hash), } } @@ -465,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); @@ -1,5 +1,5 @@ -use bytemuck::{Pod, Zeroable}; -use glam::{Mat4, Vec2}; + use bytemuck::{Pod, Zeroable}; +use glam::{Mat4, Vec2, Vec3}; use crate::math::Rect; pub struct Camera2D { @@ -19,15 +19,24 @@ impl Default for Camera2D { impl Camera2D { /// Returns the orthographic view-projection matrix for the current camera state pub fn view_proj(&self, screen_size: Vec2) -> Mat4 { - let width = screen_size.x / self.zoom; - let height = screen_size.y / self.zoom; + let (width, height) = (screen_size.x, screen_size.y); - let left = self.position.x; - let right = self.position.x + width; - let top = self.position.y; - let bottom = self.position.y + height; + let view = Mat4::look_at_rh( + self.position.extend(1.0), + self.position.extend(0.0), + Vec3::Y, + ); - Mat4::orthographic_lh(left, right, bottom, top, -1.0, 1.0) + let proj = Mat4::orthographic_rh( + 0.0, + width, + height, + 0.0, + -1.0, + 1.0, + ); + + proj * view } /// Set the camera's position (top-left corner of view) @@ -0,0 +1,218 @@ +// In widgets/layout.rs + +use std::any::Any; +use glam::{vec2, Vec2}; +use crate::{KinoState, UiNode, UiInstructionType, ContaineredWidgetType, WidgetId}; +use crate::widgets::{Anchor, ContaineredWidget}; + +fn calculate_node_size(node: &UiNode) -> Vec2 { + match &node.instruction { + UiInstructionType::Widget(widget) => widget.size(), + UiInstructionType::Containered(ContaineredWidgetType::Start { widget, .. }) => { + if let Some(row) = widget.as_any().downcast_ref::<Row>() { + calculate_row_size(&node.children, row.spacing) + } else if let Some(column) = widget.as_any().downcast_ref::<Column>() { + calculate_column_size(&node.children, column.spacing) + } else { + Vec2::ZERO + } + } + _ => Vec2::ZERO, + } +} + +fn calculate_row_size(children: &[UiNode], spacing: f32) -> Vec2 { + if children.is_empty() { + return Vec2::ZERO; + } + + let mut total_width = 0.0; + let mut max_height: f32 = 0.0; + + for (i, child) in children.iter().enumerate() { + let child_size = calculate_node_size(child); + total_width += child_size.x; + max_height = max_height.max(child_size.y); + + if i < children.len() - 1 { + total_width += spacing; + } + } + + vec2(total_width, max_height) +} + +fn calculate_column_size(children: &[UiNode], spacing: f32) -> Vec2 { + if children.is_empty() { + return Vec2::ZERO; + } + + let mut total_height = 0.0; + let mut max_width: f32 = 0.0; + + for (i, child) in children.iter().enumerate() { + let child_size = calculate_node_size(child); + total_height += child_size.y; + max_width = max_width.max(child_size.x); + + if i < children.len() - 1 { + total_height += spacing; + } + } + + vec2(max_width, total_height) +} + +pub struct Row { + pub id: WidgetId, + pub anchor: Anchor, + pub position: Vec2, + pub spacing: f32, +} + +impl Row { + pub fn new(id: impl Into<WidgetId>) -> Self { + Self { + id: id.into(), + anchor: Anchor::TopLeft, + position: Vec2::ZERO, + spacing: 8.0, + } + } + + pub fn at(mut self, position: impl Into<Vec2>) -> Self { + self.position = position.into(); + self + } + + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + + pub fn anchor(mut self, anchor: Anchor) -> Self { + self.anchor = anchor; + self + } + + pub fn build(self) -> Box<Self> { + Box::new(self) + } +} + +impl ContaineredWidget for Row { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) { + let total_size = calculate_row_size(&children, self.spacing); + + let offset = match self.anchor { + Anchor::TopLeft => Vec2::ZERO, + Anchor::Center => vec2(-total_size.x / 2.0, 0.0), + }; + + let start_pos = self.position + offset; + let mut x_offset = 0.0; + + for child in children { + let child_size = calculate_node_size(&child); + + state.push_container(crate::math::Rect::new( + start_pos + vec2(x_offset, 0.0), + vec2(child_size.x, total_size.y) + )); + + state.render_tree(vec![child]); + state.pop_container(); + + x_offset += child_size.x + self.spacing; + } + } + + fn size(&self, children: &[UiNode]) -> Vec2 { + calculate_row_size(children, self.spacing) + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} + +pub struct Column { + pub id: WidgetId, + pub anchor: Anchor, + pub position: Vec2, + pub spacing: f32, +} + +impl Column { + pub fn new(id: impl Into<WidgetId>) -> Self { + Self { + id: id.into(), + anchor: Anchor::TopLeft, + position: Vec2::ZERO, + spacing: 8.0, + } + } + + pub fn at(mut self, position: impl Into<Vec2>) -> Self { + self.position = position.into(); + self + } + + pub fn spacing(mut self, spacing: f32) -> Self { + self.spacing = spacing; + self + } + + pub fn anchor(mut self, anchor: Anchor) -> Self { + self.anchor = anchor; + self + } + + pub fn build(self) -> Box<Self> { + Box::new(self) + } +} + +impl ContaineredWidget for Column { + fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState) { + let total_size = calculate_column_size(&children, self.spacing); + + let offset = match self.anchor { + Anchor::TopLeft => Vec2::ZERO, + Anchor::Center => vec2(0.0, -total_size.y / 2.0), + }; + + let start_pos = self.position + offset; + let mut y_offset = 0.0; + + for child in children { + let child_size = calculate_node_size(&child); + + state.push_container(crate::math::Rect::new( + start_pos + vec2(0.0, y_offset), + vec2(total_size.x, child_size.y) + )); + + state.render_tree(vec![child]); + state.pop_container(); + + y_offset += child_size.y + self.spacing; + } + } + + fn size(&self, children: &[UiNode]) -> Vec2 { + calculate_column_size(children, self.spacing) + } + + fn id(&self) -> WidgetId { + self.id + } + + fn as_any(&self) -> &dyn Any { + self + } +} @@ -1,8 +1,10 @@ pub mod rect; pub mod shorthand; pub mod text; +pub mod layout; use std::any::Any; +use glam::Vec2; use crate::{KinoState, UiNode, WidgetId}; /// Determines how the object is anchored. @@ -21,12 +23,14 @@ pub trait NativeWidget: Send + Sync { /// The state is provided for you to manipulate, such as adding a new response based on the /// [`WidgetId`]. fn render(self: Box<Self>, state: &mut KinoState); + fn size(&self) -> Vec2; fn id(&self) -> WidgetId; fn as_any(&self) -> &dyn Any; } pub trait ContaineredWidget: Send + Sync { fn render(self: Box<Self>, children: Vec<UiNode>, state: &mut KinoState); + fn size(&self, children: &[UiNode]) -> Vec2; fn id(&self) -> WidgetId; fn as_any(&self) -> &dyn Any; } @@ -65,7 +65,7 @@ impl Rectangle { id: id.into(), anchor: Anchor::TopLeft, position: Vec2::ZERO, - size: vec2(64.0, 64.0), + size: vec2(64.0, 128.0), rotation: 0.0, uvs: [[0.0, 0.0], [1.0, 0.0], [1.0, 1.0], [0.0, 1.0]], fill: Fill::default(), @@ -220,6 +220,10 @@ impl NativeWidget for Rectangle { self.render_body(state, &rect, rot); } + fn size(&self) -> Vec2 { + self.size + } + fn id(&self) -> WidgetId { self.id } @@ -238,6 +242,10 @@ impl ContaineredWidget for Rectangle { state.pop_container(); } + fn size(&self, _children: &[UiNode]) -> Vec2 { + self.size + } + fn id(&self) -> WidgetId { self.id } @@ -1,4 +1,5 @@ use crate::{KinoState, WidgetId}; +use crate::widgets::layout::{Column, Row}; use crate::widgets::rect::Rectangle; use crate::widgets::text::Text; @@ -42,4 +43,38 @@ where let mut text = Text::new(text); configure(&mut text); kino.add_widget(Box::new(text)) +} + +/// Shorthand for [`Row`], used for displaying items that are displayed horizontally. +pub fn row<C>( + kino: &mut KinoState, + row: Row, + contents: C, +) -> WidgetId +where + C: FnOnce(&mut KinoState), +{ + let id = row.id; + + kino.add_container(Box::new(row)); + contents(kino); + kino.end_container(id); + id +} + +/// Shorthand for [`Column`], used for displaying items that are displayed vertically. +pub fn column<C>( + kino: &mut KinoState, + column: Column, + contents: C, +) -> WidgetId +where + C: FnOnce(&mut KinoState), +{ + let id = column.id; + + kino.add_container(Box::new(column)); + contents(kino); + kino.end_container(id); + id } @@ -126,6 +126,10 @@ impl NativeWidget for Text { }); } + fn size(&self) -> Vec2 { + self.size + } + fn id(&self) -> WidgetId { self.id } @@ -2,6 +2,7 @@ use std::sync::Arc; use glam::Vec2; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::window::Window; +use crate::KinoState; pub struct KinoWinitWindowing { // mouse @@ -9,29 +10,46 @@ pub struct KinoWinitWindowing { pub mouse_button: MouseButton, pub mouse_press_state: ElementState, - // keyboard - // windowing - _window: Arc<Window>, + window: Arc<Window>, /// The top-left most pixel pub viewport_offset: Vec2, /// Scale from screen-space to viewport texture space pub viewport_scale: Vec2, + + pub scale_factor: f32, + pub auto_scale: bool, } impl KinoWinitWindowing { /// Creates a new instance of [KinoWinitWindowing] with a specified viewport texture offset. - pub fn new(window: Arc<Window>) -> Self { + pub fn new(window: Arc<Window>, scale_factor: Option<f32>) -> Self { + let auto_scale = scale_factor.is_none(); + let scale_factor = scale_factor.unwrap_or(window.scale_factor() as f32); Self { mouse_position: Default::default(), mouse_button: MouseButton::Left, mouse_press_state: ElementState::Released, - _window: window, + window, viewport_offset: Default::default(), viewport_scale: Vec2::ONE, + scale_factor, + auto_scale, } } + /// Get the physical size of the window in pixels + pub fn physical_size(&self) -> (u32, u32) { + let size = self.window.inner_size(); + (size.width, size.height) + } + + /// Get the logical size of the window (physical size / scale_factor) + pub fn logical_size(&self) -> Vec2 { + let (w, h) = self.physical_size(); + Vec2::new(w as f32, h as f32) / self.scale_factor + } + pub(crate) fn handle_event(&mut self, event: &WindowEvent) { match event { WindowEvent::CursorMoved { position, .. } => { @@ -43,11 +61,50 @@ impl KinoWinitWindowing { self.mouse_button = *button; self.mouse_press_state = *state; } + WindowEvent::ScaleFactorChanged { scale_factor, .. } => { + if self.auto_scale { return; } + self.scale_factor = *scale_factor as f32; + } WindowEvent::Resized(_) => {} WindowEvent::KeyboardInput { .. } => {} WindowEvent::MouseWheel { .. } => {} - WindowEvent::ScaleFactorChanged { .. } => {} _ => {} } } +} + +impl KinoState { + /// Get the current DPI scale factor + pub fn scale_factor(&self) -> f32 { + self.windowing.scale_factor + } + + /// Scale a logical size to physical pixels + pub fn to_physical(&self, logical: f32) -> f32 { + logical * self.windowing.scale_factor + } + + /// Scale a logical Vec2 to physical pixels + pub fn to_physical_vec(&self, logical: Vec2) -> Vec2 { + logical * self.windowing.scale_factor + } + + /// Scale physical pixels to logical size + pub fn to_logical(&self, physical: f32) -> f32 { + physical / self.windowing.scale_factor + } + + /// Scale physical Vec2 to logical size + pub fn to_logical_vec(&self, physical: Vec2) -> Vec2 { + physical / self.windowing.scale_factor + } + + pub fn set_scale_factor(&mut self, factor: Option<f32>) { + if let Some(factor) = factor { + self.windowing.scale_factor = factor; + self.windowing.auto_scale = false; + } else { + self.windowing.auto_scale = true; + } + } } Binary files a/resources/models/cube.glb and /dev/null differ Binary files a/resources/models/error.glb and /dev/null differ @@ -1,9 +1,9 @@ package com.dropbear -import com.dropbear.asset.AssetHandle +import com.dropbear.asset.AssetType +import com.dropbear.asset.Handle import com.dropbear.ffi.NativeEngine import com.dropbear.input.InputState -import com.dropbear.logging.Logger import com.dropbear.scene.SceneManager import com.dropbear.ui.UIInstruction import com.dropbear.ui.UIInstructionSet @@ -32,17 +32,6 @@ class DropbearEngine(val native: NativeEngine) { fun getLastErrMsg(): String? { return lastErrorMessage } - - /** - * Globally sets whether exceptions should be thrown when an error occurs. - * - * This can be run in your update loop without consequences. - */ - @Deprecated("Currently not supported anymore, automatically throws exception on error. " + - "Better to catch the exception instead", level = DeprecationLevel.HIDDEN) - fun callExceptionOnError(toggle: Boolean) { - exceptionOnError = toggle - } } /** @@ -61,19 +50,10 @@ class DropbearEngine(val native: NativeEngine) { * ## Warning * The eucalyptus asset URI (or `euca://`) is case-sensitive. */ - fun getAsset(eucaURI: String): AssetHandle? { + fun <T : AssetType> getAsset(eucaURI: String): Handle<T>? { val id = com.dropbear.getAsset(eucaURI) - return if (id != null) AssetHandle(id) else null - } - - /** - * Globally sets whether exceptions should be thrown when an error occurs. - * - * This can be run in your update loop without consequences. - */ - @Deprecated("Currently not supported anymore, automatically throws exception on error. " + - "Better to catch the exception instead", level = DeprecationLevel.HIDDEN) - fun callExceptionOnError(toggle: Boolean) { + if (id == null || id <= 0L) return null + return Handle(id) } /** @@ -0,0 +1,57 @@ +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 + } + + fun setAnimation(index: Int, speed: Float = 1f) = setActiveAnimationIndex(index).let { setSpeed(speed) } +} + +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,3 @@ +package com.dropbear.asset + +open class AssetType internal constructor(open val id: Long) @@ -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(override val id: Long): AssetType(id) { + 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(override val id: Long): AssetType(id) { + 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,51 @@ +package com.dropbear.asset.model + +import com.dropbear.math.Vector3f +import com.dropbear.math.Quaternionf + +data class Animation( + val name: String, + val channels: List<AnimationChannel>, + val duration: Float +) + +data class AnimationChannel( + val targetNode: Int, + val times: DoubleArray, + val values: ChannelValues, + val interpolation: AnimationInterpolation +) { + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other == null || this::class != other::class) return false + + other as AnimationChannel + + if (targetNode != other.targetNode) return false + if (!times.contentEquals(other.times)) return false + if (values != other.values) return false + if (interpolation != other.interpolation) return false + + return true + } + + override fun hashCode(): Int { + var result = targetNode + result = 31 * result + times.contentHashCode() + result = 31 * result + values.hashCode() + result = 31 * result + interpolation.hashCode() + return result + } +} + +enum class AnimationInterpolation { + LINEAR, + STEP, + CUBIC_SPLINE +} + +sealed class ChannelValues { + data class Translations(val values: List<Vector3f>) : ChannelValues() + data class Rotations(val values: List<Quaternionf>) : ChannelValues() + data class Scales(val values: List<Vector3f>) : ChannelValues() +} @@ -0,0 +1,24 @@ +package com.dropbear.asset.model + +import com.dropbear.asset.Texture +import com.dropbear.math.Vector2f +import com.dropbear.math.Vector3f +import com.dropbear.math.Vector4f + +data class Material( + val name: String, + val diffuseTexture: Texture, + val normalTexture: Texture, + val tint: Vector4f, + val emissiveFactor: Vector3f, + val metallicFactor: Float, + val roughnessFactor: Float, + val alphaCutoff: Float?, + val doubleSided: Boolean, + val occlusionStrength: Float, + val normalScale: Float, + val uvTiling: Vector2f, + val emissiveTexture: Texture?, + val metallicRoughnessTexture: Texture?, + val occlusionTexture: Texture? +) @@ -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,16 @@ +package com.dropbear.asset.model + +import com.dropbear.math.Vector2f +import com.dropbear.math.Vector3f +import com.dropbear.math.Vector4f + +data class ModelVertex( + val position: Vector3f, + val normal: Vector3f, + val tangent: Vector4f, + val texCoords0: Vector2f, + val texCoords1: Vector2f, + val colour0: Vector4f, + val joints0: IntArray, + val weights0: Vector4f +) @@ -0,0 +1,17 @@ +package com.dropbear.asset.model + +import com.dropbear.math.Vector3f +import com.dropbear.math.Quaternionf + +data class NodeTransform( + val translation: Vector3f, + val rotation: Quaternionf, + val scale: Vector3f +) + +data class Node( + val name: String, + val parent: Int?, + val children: List<Int>, + val transform: NodeTransform +) @@ -0,0 +1,8 @@ +package com.dropbear.asset.model + +data class Skin( + val name: String, + val joints: List<Int>, + val inverseBindMatrices: List<DoubleArray>, + val skeletonRoot: Int? +) @@ -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()) @@ -0,0 +1,14 @@ +package com.dropbear.ui.styling + +enum class Anchor { + /** + * A center anchor is when the position is based on the center of the object (such as the + * center of a circle) + */ + Center, + + /** + * A top left anchor is when the position is based on the top left corner of the object. + */ + TopLeft, +} @@ -0,0 +1,5 @@ +package com.dropbear.ui.styling + +import com.dropbear.utils.Colour + +data class Fill(var colour: Colour) @@ -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) { - - } - } -} @@ -0,0 +1,70 @@ +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.styling.Anchor + +class Column( + override var id: WidgetId, + var anchor: Anchor = Anchor.TopLeft, + var position: Vector2d = Vector2d.zero(), + var spacing: Double = 8.0, +): Widget() { + sealed class ColumnInstruction: UIInstruction { + data class StartColumnBlock(val id: WidgetId, val column: com.dropbear.ui.widgets.Column) : ColumnInstruction() + data class EndColumnBlock(val id: WidgetId) : ColumnInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(ColumnInstruction.StartColumnBlock(id, this), ColumnInstruction.EndColumnBlock(id)) + } + + fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> { + return listOf(ColumnInstruction.StartColumnBlock(id, this)) + + children + + ColumnInstruction.EndColumnBlock(id) + } + + fun startInstruction(): UIInstruction = ColumnInstruction.StartColumnBlock(id, this) + + fun endInstruction(): UIInstruction = ColumnInstruction.EndColumnBlock(id) + + fun at(position: Vector2d): Column { + this.position = position + return this + } + + fun spacing(spacing: Double): Column { + this.spacing = spacing + return this + } + + fun withAnchor(anchor: Anchor): Column { + this.anchor = anchor + return this + } +} + +fun UIBuilder.column(id: WidgetId = WidgetId(generateId().toLong()), block: Column.() -> Unit = {}): Column { + val column = Column(id = id).apply(block) + column.toInstruction().forEach { instructions.add(it) } + return column +} + +fun UIBuilder.column(id: WidgetId, content: UIBuilder.(Column) -> Unit) { + column(id, columnBlock = {}, content = content) +} + +fun UIBuilder.column( + id: WidgetId, + columnBlock: Column.() -> Unit = {}, + content: UIBuilder.(Column) -> Unit +) { + val column = Column(id = id).apply(columnBlock) + instructions.add(column.startInstruction()) + this.content(column) + instructions.add(column.endInstruction()) +} @@ -0,0 +1,118 @@ +package com.dropbear.ui.widgets + +import com.dropbear.asset.Handle +import com.dropbear.asset.Texture +import com.dropbear.math.Angle +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.styling.Anchor +import com.dropbear.ui.styling.Border +import com.dropbear.ui.styling.Fill +import com.dropbear.utils.Colour + +class Rectangle( + override var id: WidgetId, + var anchor: Anchor = Anchor.TopLeft, + var position: Vector2d = Vector2d.zero(), + var size: Vector2d = Vector2d(64.0, 128.0), + var texture: Handle<Texture>? = null, + var rotation: Angle = Angle.ZERO, + var uv: List<Vector2d> = listOf(Vector2d.zero(), Vector2d(1.0, 0.0), Vector2d.one(), Vector2d(0.0, 1.0)), + var fill: Fill = Fill(Colour.WHITE), + var border: Border? = null +): Widget() { + init { + require(uv.size == 4) { "Rectangle uv must have 4 coordinates." } + } + + sealed class RectangleInstruction: UIInstruction { + data class Rectangle(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction() + data class StartRectangleBlock(val id: WidgetId, val rect: com.dropbear.ui.widgets.Rectangle) : RectangleInstruction() + data class EndRectangleBlock(val id: WidgetId) : RectangleInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(RectangleInstruction.Rectangle(id, this)) + } + + fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> { + return listOf(RectangleInstruction.StartRectangleBlock(id, this)) + + children + + RectangleInstruction.EndRectangleBlock(id) + } + + fun startInstruction(): UIInstruction = RectangleInstruction.StartRectangleBlock(id, this) + + fun endInstruction(): UIInstruction = RectangleInstruction.EndRectangleBlock(id) + + fun with(position: Vector2d, size: Vector2d): Rectangle { + this.position = position + this.size = size + return this + } + + fun withAnchor(anchor: Anchor): Rectangle { + this.anchor = anchor + return this + } + + fun at(position: Vector2d): Rectangle { + this.position = position + return this + } + + fun size(size: Vector2d): Rectangle { + this.size = size + return this + } + + fun fill(fill: Fill): Rectangle { + this.fill = fill + return this + } + + fun border(border: Border?): Rectangle { + this.border = border + return this + } + + fun rotate(angle: Angle): Rectangle { + this.rotation = angle + return this + } + + fun texture(texture: Handle<Texture>?): Rectangle { + this.texture = texture + return this + } + + fun uv(coords: List<Vector2d>): Rectangle { + require(coords.size == 4) { "Rectangle uv must have 4 coordinates." } + this.uv = coords + return this + } +} + +fun UIBuilder.rectangle(id: WidgetId = WidgetId(generateId().toLong()), block: Rectangle.() -> Unit = {}): Rectangle { + val rect = Rectangle(id = id).apply(block) + rect.toInstruction().forEach { instructions.add(it) } + return rect +} + +fun UIBuilder.container(id: WidgetId, block: UIBuilder.(Rectangle) -> Unit) { + container(id, rectBlock = {}, content = block) +} + +fun UIBuilder.container( + id: WidgetId, + rectBlock: Rectangle.() -> Unit = {}, + content: UIBuilder.(Rectangle) -> Unit +) { + val rect = Rectangle(id = id).apply(rectBlock) + instructions.add(rect.startInstruction()) + this.content(rect) + instructions.add(rect.endInstruction()) +} @@ -0,0 +1,70 @@ +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.styling.Anchor + +class Row( + override var id: WidgetId, + var anchor: Anchor = Anchor.TopLeft, + var position: Vector2d = Vector2d.zero(), + var spacing: Double = 8.0, +): Widget() { + sealed class RowInstruction: UIInstruction { + data class StartRowBlock(val id: WidgetId, val row: com.dropbear.ui.widgets.Row) : RowInstruction() + data class EndRowBlock(val id: WidgetId) : RowInstruction() + } + + override fun toInstruction(): List<UIInstruction> { + return listOf(RowInstruction.StartRowBlock(id, this), RowInstruction.EndRowBlock(id)) + } + + fun toContaineredInstructions(children: List<UIInstruction>): List<UIInstruction> { + return listOf(RowInstruction.StartRowBlock(id, this)) + + children + + RowInstruction.EndRowBlock(id) + } + + fun startInstruction(): UIInstruction = RowInstruction.StartRowBlock(id, this) + + fun endInstruction(): UIInstruction = RowInstruction.EndRowBlock(id) + + fun at(position: Vector2d): Row { + this.position = position + return this + } + + fun spacing(spacing: Double): Row { + this.spacing = spacing + return this + } + + fun withAnchor(anchor: Anchor): Row { + this.anchor = anchor + return this + } +} + +fun UIBuilder.row(id: WidgetId = WidgetId(generateId().toLong()), block: Row.() -> Unit = {}): Row { + val row = Row(id = id).apply(block) + row.toInstruction().forEach { instructions.add(it) } + return row +} + +fun UIBuilder.row(id: WidgetId, content: UIBuilder.(Row) -> Unit) { + row(id, rowBlock = {}, content = content) +} + +fun UIBuilder.row( + id: WidgetId, + rowBlock: Row.() -> Unit = {}, + content: UIBuilder.(Row) -> Unit +) { + val row = Row(id = id).apply(rowBlock) + instructions.add(row.startInstruction()) + this.content(row) + instructions.add(row.endInstruction()) +} @@ -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? {