tirbofish/dropbear · diff
wip: asset server remake and improved model importing
wip: animation within gltf
Signature present but could not be verified.
Unverified
@@ -1,513 +1,222 @@ -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() - } - - pub fn iter_model_raw(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Model>> { - self.models.iter() - } - - pub fn iter_material(&self) -> dashmap::iter::Iter<'_, AssetHandle, Arc<Material>> { - self.iter_material_raw() - } - - 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())) + /// 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 } - /// Fetches a material by handle. - pub fn get_material(&self, handle: AssetHandle) -> Option<Arc<Material>> { - self.get_material_raw(handle) + /// 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 get_material_raw(&self, handle: AssetHandle) -> Option<Arc<Material>> { - self.materials - .get(&handle) - .map(|entry| Arc::clone(entry.value())) + /// 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()); } - /// 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 +224,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() -} +} @@ -12,6 +12,7 @@ use glam::{DMat4, DVec3}; use std::fmt::{Display, Formatter}; use std::sync::Arc; use wgpu::{BindGroup}; +use crate::asset::ASSET_REGISTRY; pub const MAX_LIGHTS: usize = 10; @@ -288,10 +289,11 @@ impl Light { log::trace!("Created new light uniform"); - let cube_model = Model::load_from_memory( + let cube_model = Model::load_from_memory_raw( graphics.clone(), include_bytes!("../../../resources/models/cube.glb").to_vec(), label, + ASSET_REGISTRY.clone(), None ) .await @@ -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 parking_lot::{Mutex, RwLock}; use rayon::prelude::*; use serde::{Deserialize, Serialize}; use std::collections::HashMap; @@ -16,164 +15,17 @@ 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 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 label: String, + pub(crate) hash: u64, pub path: ResourceReference, pub meshes: Vec<Mesh>, pub materials: Vec<Material>, - pub id: ModelId, -} - -/// 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>, -} - -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) - } -} - -impl Deref for SharedShape { - type Target = Model; - - fn deref(&self) -> &Self::Target { - self.model.as_ref() - } -} - -#[derive(Clone)] -pub struct LoadedModel { - pub(crate) inner: Arc<SharedShape>, - handle: AssetHandle, -} - -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 - } - - 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 contains_material_reference_raw( - &self, - registry: &AssetRegistry, - reference: &ResourceReference, - ) -> bool { - self.inner - .contains_material_reference_raw(registry, reference) - } -} - -impl std::ops::Deref for LoadedModel { - type Target = Model; - - fn deref(&self) -> &Self::Target { - self.inner.deref() - } } #[derive(Clone)] @@ -183,11 +35,22 @@ pub struct Material { 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 tint_bind_group: wgpu::BindGroup, 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 Material { @@ -196,27 +59,25 @@ 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(); + 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 { @@ -236,11 +97,22 @@ 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, } } @@ -250,54 +122,46 @@ impl Material { normal: &Texture, 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), - }, - ], - }) + graphics.device.create_bind_group(&wgpu::BindGroupDescriptor { + label: Some(format!("{} texture 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), + }, + ], + }) } - 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(Clone)] @@ -310,244 +174,512 @@ pub struct Mesh { pub vertices: Vec<ModelVertex>, } +struct GLTFTextureInformation<'a> { + sampler: wgpu::SamplerDescriptor<'a>, + pixels: Vec<u8>, + mip_level_count: u32, + format: wgpu::TextureFormat, +} + +struct GLTFMeshInformation { + name: String, + primitive_index: usize, + material_index: usize, + mode: gltf::mesh::Mode, + positions: Vec<[f32; 3]>, + indices: Vec<u32>, + normals: Vec<[f32; 3]>, + tangents: Vec<[f32; 4]>, + colors: Vec<[f32; 4]>, + joints: Vec<[u16; 4]>, + weights: Vec<[f32; 4]>, + tex_coords0: Vec<[f32; 2]>, + tex_coords1: Vec<[f32; 2]>, +} + +struct GLTFMaterialInformation { + name: String, + diffuse_bytes: Option<Vec<u8>>, + normal_bytes: Option<Vec<u8>>, + emissive_bytes: Option<Vec<u8>>, + metallic_roughness_bytes: Option<Vec<u8>>, + occlusion_bytes: Option<Vec<u8>>, + diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>, + normal_sampler: Option<wgpu::SamplerDescriptor<'static>>, + emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>, + metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>, + occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>, + tint: [f32; 4], + emissive_factor: [f32; 3], + metallic_factor: f32, + roughness_factor: f32, + alpha_mode: gltf::material::AlphaMode, + alpha_cutoff: Option<f32>, + double_sided: bool, + occlusion_strength: f32, + normal_scale: f32, +} + +struct ProcessedMaterialTextures { + name: String, + diffuse: Option<(Vec<u8>, (u32, u32))>, + normal: Option<(Vec<u8>, (u32, u32))>, + emissive: Option<(Vec<u8>, (u32, u32))>, + metallic_roughness: Option<(Vec<u8>, (u32, u32))>, + occlusion: Option<(Vec<u8>, (u32, u32))>, + diffuse_sampler: Option<wgpu::SamplerDescriptor<'static>>, + normal_sampler: Option<wgpu::SamplerDescriptor<'static>>, + emissive_sampler: Option<wgpu::SamplerDescriptor<'static>>, + metallic_roughness_sampler: Option<wgpu::SamplerDescriptor<'static>>, + occlusion_sampler: Option<wgpu::SamplerDescriptor<'static>>, + tint: [f32; 4], + emissive_factor: [f32; 3], + metallic_factor: f32, + roughness_factor: f32, + alpha_mode: gltf::material::AlphaMode, + alpha_cutoff: Option<f32>, + double_sided: bool, + occlusion_strength: f32, + normal_scale: f32, +} + impl Model { - /// 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 + fn sampler_descriptor_for_texture(texture: &gltf::Texture<'_>) -> wgpu::SamplerDescriptor<'static> { + let sampler = texture.sampler(); + + let mag_filter = match sampler.mag_filter() { + Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest, + _ => wgpu::FilterMode::Linear, + }; + + let (min_filter, mipmap_filter) = match sampler.min_filter() { + Some(MinFilter::Nearest) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest), + Some(MinFilter::Linear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest), + Some(MinFilter::NearestMipmapNearest) => { + (wgpu::FilterMode::Nearest, wgpu::FilterMode::Nearest) + } + Some(MinFilter::LinearMipmapNearest) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Nearest), + Some(MinFilter::NearestMipmapLinear) => (wgpu::FilterMode::Nearest, wgpu::FilterMode::Linear), + Some(MinFilter::LinearMipmapLinear) => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear), + None => (wgpu::FilterMode::Linear, wgpu::FilterMode::Linear), + }; + + fn map_wrap(wrap: gltf::texture::WrappingMode) -> wgpu::AddressMode { + match wrap { + gltf::texture::WrappingMode::ClampToEdge => wgpu::AddressMode::ClampToEdge, + gltf::texture::WrappingMode::MirroredRepeat => wgpu::AddressMode::MirrorRepeat, + gltf::texture::WrappingMode::Repeat => wgpu::AddressMode::Repeat, + } } - } - /// 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 + 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 `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) - } + fn load_texture<'a>(tex: &'a gltf::Texture<'_>, images: &Vec<gltf::image::Data>) -> GLTFTextureInformation<'a> { + let sampler = tex.sampler(); - /// Returns the registered asset handle for this model, if available. - pub fn asset_handle(&self) -> Option<AssetHandle> { - self.asset_handle_raw(&ASSET_REGISTRY) - } + let mag_filter = match sampler.mag_filter() { + Some(gltf::texture::MagFilter::Nearest) => wgpu::FilterMode::Nearest, + _ => wgpu::FilterMode::Linear, + }; - pub fn asset_handle_raw(&self, registry: &AssetRegistry) -> Option<AssetHandle> { - registry.model_handle_from_reference(&self.path) - } + 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), + }; - /// Returns `true` if this model was loaded from the specified resource reference. - pub fn matches_resource(&self, reference: &ResourceReference) -> bool { - &self.path == reference - } + 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 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 sampler = wgpu::SamplerDescriptor { + label: Some(tex.name().unwrap_or("Unnamed Texture Sampler")), + 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, + }; - pub fn contains_material_handle_raw( - &self, - registry: &AssetRegistry, - material_handle: AssetHandle, - ) -> bool { - registry.material_owner(material_handle) == Some(self.id) - } + let image_index = tex.source().index(); + let image_data = &images[image_index]; - /// 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) - } + let width = image_data.width; + let height = image_data.height; - 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) - }) - } + let mip_level_count = (width.max(height) as f32).log2().floor() as u32 + 1; + + 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"), + }; - /// 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)) + GLTFTextureInformation { + sampler, + mip_level_count, + pixels, + format, + } } - /// 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) + fn load_materials( + gltf: &gltf::Document, + buffers: &Vec<gltf::buffer::Data>, + ) -> Vec<GLTFMaterialInformation> { + let read_texture_bytes = |texture: gltf::Texture<'_>| -> Option<Vec<u8>> { + let image = texture.source(); + match image.source() { + gltf::image::Source::View { view, mime_type: _ } => { + let buffer_data = &buffers[view.buffer().index()]; + let start = view.offset(); + let end = start + view.length(); + Some(buffer_data[start..end].to_vec()) + } + gltf::image::Source::Uri { uri, mime_type: _ } => { + log::warn!("External URI textures not supported: {}", uri); + None + } + } + }; + + let mut material_data = Vec::new(); + + for material in gltf.materials() { + log::debug!("Processing material: {:?}", material.name()); + let material_name = material.name().unwrap_or("Unnamed Material").to_string(); + + 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_bytes = diffuse_texture + .as_ref() + .and_then(|info| read_texture_bytes(info.texture())); + let metallic_roughness_bytes = metallic_roughness_texture + .as_ref() + .and_then(|info| read_texture_bytes(info.texture())); + + let normal_bytes = normal_texture + .as_ref() + .and_then(|info| read_texture_bytes(info.texture())); + let occlusion_bytes = occlusion_texture + .as_ref() + .and_then(|info| read_texture_bytes(info.texture())); + let emissive_bytes = emissive_texture + .as_ref() + .and_then(|info| read_texture_bytes(info.texture())); + + let diffuse_sampler = diffuse_texture + .as_ref() + .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); + let metallic_roughness_sampler = metallic_roughness_texture + .as_ref() + .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); + let normal_sampler = normal_texture + .as_ref() + .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); + let occlusion_sampler = occlusion_texture + .as_ref() + .map(|info| Self::sampler_descriptor_for_texture(&info.texture())); + let emissive_sampler = emissive_texture + .as_ref() + .map(|info| Self::sampler_descriptor_for_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_bytes, + normal_bytes, + emissive_bytes, + metallic_roughness_bytes, + occlusion_bytes, + 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, + }); + } + + if material_data.is_empty() { + material_data.push(GLTFMaterialInformation { + name: "Default".to_string(), + diffuse_bytes: None, + normal_bytes: None, + emissive_bytes: None, + metallic_roughness_bytes: None, + occlusion_bytes: None, + diffuse_sampler: None, + normal_sampler: None, + emissive_sampler: None, + metallic_roughness_sampler: None, + occlusion_sampler: None, + 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, + }); + } + + material_data } - 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 + 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(); + + for (primitive_index, primitive) in mesh.primitives().enumerate() { + 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 + )) + } + }; + + 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, + }); + } + + Ok(()) } pub async fn load_from_memory_raw<B>( graphics: Arc<SharedGraphicsContext>, buffer: B, label: Option<&str>, - registry: &AssetRegistry, - cache: &Mutex<HashMap<String, Arc<Model>>>, + registry: Arc<RwLock<AssetRegistry>>, import_scale: Option<f64>, - ) -> anyhow::Result<LoadedModel> + ) -> anyhow::Result<Handle<Model>> 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 - ); + let mut registry = registry.write(); - 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 model_label = label.unwrap_or("No named model"); + let hash = if let Some(label) = label { + let mut hasher = DefaultHasher::default(); + label.hash(&mut hasher); + hasher.finish() + } else { + let mut hasher = DefaultHasher::default(); + buffer.as_ref().hash(&mut hasher); + hasher.finish() + }; - log::trace!( - "========== Benchmarking speed of loading {:?} ==========", - label - ); - log::debug!("Loading from memory"); - let res_ref = ResourceReference::from_bytes(buffer.as_ref()); + if let Some(model) = registry.model_handle_by_hash(hash) { + return Ok(model); + } let (gltf, buffers, _images) = gltf::import_slice(buffer.as_ref())?; - let mut meshes = Vec::new(); - - 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(); - - let tint = material - .pbr_metallic_roughness() - .base_color_factor(); - - let tint = [tint[0], tint[1], tint[2], tint[3]]; - - 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 - } - } - } else { - None - }; - - 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()) - } - gltf::image::Source::Uri { uri, mime_type: _ } => { - log::warn!("External URI textures not supported: {}", uri); - None - } - } - } else { - None - }; - texture_data.push((material_name, diffuse_bytes, normal_bytes, tint)); + let mut meshes = Vec::new(); + for mesh in gltf.meshes() { + Self::load_meshes(&mesh, &buffers, &mut meshes)?; } - if texture_data.is_empty() { - texture_data.push(( - "Default".to_string(), - None, - None, - [1.0, 1.0, 1.0, 1.0], - )); - } + let material_data = Self::load_materials(&gltf, &buffers); let parallel_start = Instant::now(); - let processed_textures: Vec<_> = texture_data + let processed_textures: Vec<ProcessedMaterialTextures> = material_data .into_par_iter() - .map(|(material_name, diffuse_bytes, normal_bytes, tint)| { + .map(|material_info| { let material_start = Instant::now(); - let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| { + let material_name = material_info.name; + let diffuse_bytes = material_info.diffuse_bytes; + let normal_bytes = material_info.normal_bytes; + let emissive_bytes = material_info.emissive_bytes; + let metallic_roughness_bytes = material_info.metallic_roughness_bytes; + let occlusion_bytes = material_info.occlusion_bytes; + let diffuse_sampler = material_info.diffuse_sampler; + let normal_sampler = material_info.normal_sampler; + let emissive_sampler = material_info.emissive_sampler; + let metallic_roughness_sampler = material_info.metallic_roughness_sampler; + let occlusion_sampler = material_info.occlusion_sampler; + let 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; + + let decode_rgba = |bytes: &Vec<u8>, label: &str| { let load_start = Instant::now(); let image = match image::load_from_memory(bytes) { Ok(image) => image, Err(err) => { log::warn!( - "Failed to decode diffuse texture for material '{}': {} ({} bytes)", + "Failed to decode {} texture for material '{}': {} ({} bytes)", + label, material_name, err, bytes.len() @@ -555,44 +687,35 @@ impl Model { return None; } }; - log::trace!("Loading diffuse image to memory: {:?}", load_start.elapsed()); + log::trace!("Loading {} image to memory: {:?}", label, load_start.elapsed()); let rgba_start = Instant::now(); let rgba = image.to_rgba8(); log::trace!( - "Converting diffuse image to rgba8 took {:?}", + "Converting {} image to rgba8 took {:?}", + label, rgba_start.elapsed() ); let dimensions = image.dimensions(); Some((rgba.into_raw(), dimensions)) - }); + }; + let processed_diffuse = diffuse_bytes.as_ref().and_then(|bytes| { + decode_rgba(bytes, "diffuse") + }); let processed_normal = normal_bytes.as_ref().and_then(|bytes| { - 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)) + decode_rgba(bytes, "normal") + }); + let processed_emissive = emissive_bytes.as_ref().and_then(|bytes| { + decode_rgba(bytes, "emissive") + }); + let processed_metallic_roughness = + metallic_roughness_bytes.as_ref().and_then(|bytes| { + decode_rgba(bytes, "metallic_roughness") + }); + let processed_occlusion = occlusion_bytes.as_ref().and_then(|bytes| { + decode_rgba(bytes, "occlusion") }); log::trace!( @@ -601,7 +724,28 @@ impl Model { material_start.elapsed() ); - (material_name, processed_diffuse, processed_normal, tint) + 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(); @@ -616,20 +760,34 @@ impl Model { 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 { + for processed in processed_textures { let start = Instant::now(); + 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( graphics.clone(), &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 +796,155 @@ 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, - )); + ); + + 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); log::trace!("Time to create GPU texture: {:?}", start.elapsed()); } - 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 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 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; - } + 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], + }); + } - 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 scale = import_scale.unwrap_or(1.0) as f32; + if (scale - 1.0).abs() > f32::EPSILON { + for vertex in &mut vertices { + vertex.position[0] *= scale; + vertex.position[1] *= scale; + vertex.position[2] *= scale; } - - let vertex_buffer = - graphics - .device - .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 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(); - } + }; - 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> { @@ -1083,50 +1127,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 +1135,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 +1163,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, @@ -11,42 +11,55 @@ impl ProcedurallyGeneratedObject { 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,13 @@ //! 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 serde::{Deserialize, Serialize}; use wgpu::util::DeviceExt; @@ -45,24 +43,8 @@ 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: &mut AssetRegistry, + ) -> Handle<Model> { let mut hasher = DefaultHasher::new(); hasher.write(bytemuck::cast_slice(&self.vertices)); hasher.write(bytemuck::cast_slice(&self.indices)); @@ -72,11 +54,8 @@ 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); + if let Some(handle) = registry.model_handle_by_hash(hash) { + return handle; } let vertices = self.vertices; @@ -108,31 +87,35 @@ 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 = registry.grey_texture(graphics.clone()); + let flat_normal_handle = + registry.solid_texture_rgba8(graphics.clone(), [128, 128, 255, 255]); + let grey = registry + .get_texture(grey_handle) + .expect("Grey texture handle missing") + .clone(); + let flat_normal = registry + .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)); - } + }; - LoadedModel::new_raw(registry, model) + registry.add_model_with_label(label, model) } } @@ -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>, } @@ -68,9 +72,15 @@ struct InstanceInput { struct VertexInput { @location(0) position: vec3<f32>, - @location(1) tex_coords: vec2<f32>, @location(2) normal: vec3<f32>, @location(3) tangent: vec3<f32>, + @location(1) tex_coords0: vec2<f32>, + @location(1) tex_coords1: vec2<f32>, + @location(1) colour0: vec4<f32>, + @location(1) joints(0): vec4<u32>, + @location(1) joints(0): vec4<u32>, + + @location(4) bitangent: vec3<f32>, }; @@ -216,12 +226,16 @@ fn s_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { discard; } + let normal = normalize(world_normal); + let tangent = normalize(world_tangent.xyz); + let bitangent = cross(n, t) * world_tangent.w; + let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); let world_normal = apply_normal_map( in.world_normal, in.world_tangent, - in.world_bitangent, + bitangent, object_normal.xyz, ); @@ -258,12 +272,16 @@ fn u_fs_main(in: VertexOutput) -> @location(0) vec4<f32> { discard; } + let normal = normalize(world_normal); + let tangent = normalize(world_tangent.xyz); + let bitangent = cross(n, t) * world_tangent.w; + let view_dir = normalize(u_camera.view_pos.xyz - in.world_position); let world_normal = apply_normal_map( in.world_normal, in.world_tangent, - in.world_bitangent, + bitangent, object_normal.xyz, ); @@ -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 { @@ -137,6 +138,7 @@ impl Texture { view, sampler, size, + hash: None, } } @@ -184,6 +186,7 @@ impl Texture { size, view, label: label.to_potential_string(), + hash: None, } } @@ -222,6 +225,7 @@ impl Texture { sampler, size, view, + hash: None, } } @@ -285,6 +289,8 @@ impl Texture { sampler: Option<wgpu::SamplerDescriptor>, label: Option<&str>, ) -> Self { + let hash = AssetRegistry::hash_bytes(bytes); + let (diffuse_rgba, dimensions) = match image::load_from_memory(bytes) { Ok(image) => { let rgba = image.to_rgba8().into_raw(); @@ -416,6 +422,7 @@ impl Texture { sampler, size, view, + hash: Some(hash), } } Binary files a/resources/models/cube.glb and /dev/null differ Binary files a/resources/models/error.glb and /dev/null differ