tirbofish/dropbear · diff
feature: implemented an import scale (fixes #59)
fix: locked axis
ci: removed mold support from windows and macos
jarvis, run github actions
Signature present but could not be verified.
Unverified
@@ -1,6 +1,3 @@ [target.x86_64-unknown-linux-gnu] linker = "clang" -rustflags = ["-C", "link-arg=-fuse-ld=mold"] - -[target.x86_64-pc-windows-msvc] rustflags = ["-C", "link-arg=-fuse-ld=mold"] @@ -172,6 +172,7 @@ pub struct MeshRenderer { 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)] @@ -196,12 +197,12 @@ impl MeshRenderer { label: Option<&str>, ) -> anyhow::Result<Self> { let path = path.as_ref().to_path_buf(); - let handle = Model::load(graphics, &path, label).await?; + let handle = Model::load(graphics, &path, label, None).await?; Ok(Self::from_handle(handle)) } - /// Creates a new [`MeshRenderer`] instance from a [`LoadedModel`] handle - pub fn from_handle(handle: LoadedModel) -> Self { + /// 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 { handle, instance: Instance::new(DVec3::ZERO, DQuat::IDENTITY, DVec3::ONE), @@ -210,9 +211,15 @@ impl MeshRenderer { 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() } @@ -238,10 +245,10 @@ impl MeshRenderer { } pub fn update(&mut self, transform: &Transform) { - let import_scale = ASSET_REGISTRY.model_import_scale(&self.handle.inner.path); - let scaled = transform.scale * glam::DVec3::splat(import_scale as f64); + // Import scaling is per-renderer and should not mutate shared model buffers. + let scale = transform.scale * glam::DVec3::splat(self.import_scale as f64); let current_matrix = DMat4::from_scale_rotation_translation( - scaled, + scale, transform.rotation, transform.position, ); @@ -599,6 +606,29 @@ impl MeshRenderer { 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_custom_import_scale(&mut self, scale: Option<f32>) { + if let Some(scale) = scale { + self.import_scale = scale; + } + } } #[repr(C)] @@ -289,6 +289,7 @@ impl Light { graphics.clone(), include_bytes!("../../resources/models/cube.glb").to_vec(), label, + None ) .await .expect("failed to load light cube model") @@ -302,6 +302,7 @@ pub struct Mesh { pub index_buffer: wgpu::Buffer, pub num_elements: u32, pub material: usize, + pub vertices: Vec<ModelVertex>, } impl Model { @@ -414,6 +415,7 @@ impl Model { graphics: Arc<SharedGraphicsContext>, buffer: B, label: Option<&str>, + import_scale: Option<f64>, ) -> anyhow::Result<LoadedModel> where B: AsRef<[u8]>, @@ -424,6 +426,7 @@ impl Model { label, &ASSET_REGISTRY, LazyLock::force(&MODEL_CACHE), + import_scale ) .await } @@ -434,6 +437,7 @@ impl Model { label: Option<&str>, registry: &AssetRegistry, cache: &Mutex<HashMap<String, Arc<Model>>>, + import_scale: Option<f64>, ) -> anyhow::Result<LoadedModel> where B: AsRef<[u8]>, @@ -441,7 +445,12 @@ impl Model { let start = Instant::now(); let mut hasher = DefaultHasher::new(); - let cache_key = label.unwrap_or("default").to_string(); + let scale_key = import_scale.unwrap_or(1.0); + let cache_key = format!( + "{}::import_scale={:.8}", + label.unwrap_or("default"), + scale_key + ); if let Some(cached_model) = { let cache_guard = cache.lock(); @@ -640,6 +649,18 @@ impl Model { 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)); @@ -739,6 +760,7 @@ impl Model { name: mesh.name().unwrap_or("Unnamed Mesh").to_string(), vertex_buffer, index_buffer, + vertices, num_elements: indices.len() as u32, material: material_index, }); @@ -772,6 +794,7 @@ impl Model { graphics: Arc<SharedGraphicsContext>, path: &PathBuf, label: Option<&str>, + import_scale: Option<f64>, ) -> anyhow::Result<LoadedModel> { Self::load_raw( graphics, @@ -779,6 +802,7 @@ impl Model { label, &ASSET_REGISTRY, LazyLock::force(&MODEL_CACHE), + import_scale ) .await } @@ -789,11 +813,13 @@ impl Model { 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 path_str = path.to_string_lossy().to_string(); + 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) = { @@ -808,7 +834,9 @@ impl Model { 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).await?; + 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) { @@ -823,7 +851,8 @@ impl Model { let mut cache_guard = cache.lock(); cache_guard.insert(path_str.clone(), Arc::clone(&updated)); if let Some(custom_label) = label { - cache_guard.insert(custom_label.to_string(), Arc::clone(&updated)); + let label_key = format!("{}::import_scale={:.8}", custom_label, scale_key); + cache_guard.insert(label_key, Arc::clone(&updated)); } } @@ -65,11 +65,14 @@ impl ProcedurallyGeneratedObject { return LoadedModel::new_raw(registry, cached_model); } + let vertices = self.vertices; + let indices = self.indices; + let vertex_buffer = graphics .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{label} Vertex Buffer")), - contents: bytemuck::cast_slice(&self.vertices), + contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX, }); @@ -77,7 +80,7 @@ impl ProcedurallyGeneratedObject { .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some(&format!("{label} Index Buffer")), - contents: bytemuck::cast_slice(&self.indices), + contents: bytemuck::cast_slice(&indices), usage: wgpu::BufferUsages::INDEX, }); @@ -85,8 +88,9 @@ impl ProcedurallyGeneratedObject { name: label.clone(), vertex_buffer, index_buffer, - num_elements: self.indices.len() as u32, + num_elements: indices.len() as u32, material: 0, + vertices, }; let material = material.unwrap_or_else(|| { @@ -99,6 +99,31 @@ impl PhysicsState { let pos = transform.position.as_vec3().to_array(); let rot = transform.rotation.as_quat().to_array(); + let mut bits = LockedAxes::empty(); + + let translation_lock = rigid_body.lock_translation; + if translation_lock.x { + bits = bits | LockedAxes::TRANSLATION_LOCKED_X; + } + if translation_lock.y { + bits = bits | LockedAxes::TRANSLATION_LOCKED_Y; + } + if translation_lock.z { + bits = bits | LockedAxes::TRANSLATION_LOCKED_Z; + } + + let rotation_lock = rigid_body.lock_rotation; + if rotation_lock.x { + bits = bits | LockedAxes::ROTATION_LOCKED_X; + } + if rotation_lock.y { + bits = bits | LockedAxes::ROTATION_LOCKED_Y; + } + if rotation_lock.z { + bits = bits | LockedAxes::ROTATION_LOCKED_Z; + } + + let body = RigidBodyBuilder::new(mode) .translation(Vector::from_array(pos)) .rotation(UnitQuaternion::from_quaternion(Quaternion::new( @@ -112,8 +137,7 @@ impl PhysicsState { .angvel(Vector::from_array(rigid_body.angvel)) .linear_damping(rigid_body.linear_damping) .angular_damping(rigid_body.angular_damping) - .enabled_translations(!rigid_body.lock_translation.x, !rigid_body.lock_translation.y, !rigid_body.lock_translation.z) - .enabled_rotations(!rigid_body.lock_rotation.x, !rigid_body.lock_rotation.y, !rigid_body.lock_rotation.z) + .locked_axes(bits) .build(); let body_handle = self.bodies.insert(body); @@ -142,6 +142,7 @@ impl SceneConfig { builder.add(*transform); } else if let Some(renderer) = component.as_any().downcast_ref::<SerializedMeshRenderer>() { let renderer = renderer.clone(); + let import_scale = renderer.import_scale.unwrap_or(1.0); let mut model = match &renderer.handle.ref_type { ResourceReferenceType::None => { log::error!( @@ -161,7 +162,7 @@ impl SceneConfig { id: ModelId(*id), }); let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model); - MeshRenderer::from_handle(loaded) + MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } ResourceReferenceType::File(reference) => { let path = &renderer.handle.resolve()?; @@ -173,15 +174,16 @@ impl SceneConfig { reference ); - MeshRenderer::from_path(graphics.clone(), &path, Some(label)).await? + let loaded = Model::load(graphics.clone(), path, Some(label), None).await?; + MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } ResourceReferenceType::Bytes(bytes) => { log::info!("Loading entity from bytes [Len: {}]", bytes.len()); - let model = - Model::load_from_memory(graphics.clone(), bytes.clone(), Some(label)) + let loaded = + Model::load_from_memory(graphics.clone(), bytes.clone(), Some(label), None) .await?; - MeshRenderer::from_handle(model) + MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } ResourceReferenceType::Cuboid { size_bits } => { let size = [ @@ -204,7 +206,7 @@ impl SceneConfig { loaded_model.refresh_registry(); - MeshRenderer::from_handle(loaded_model) + MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale) } }; @@ -220,7 +222,7 @@ impl SceneConfig { ) { let source_path = override_entry.source_model.resolve()?; let label_hint = override_entry.source_model.as_uri(); - Model::load(graphics.clone(), &source_path, label_hint).await?; + Model::load(graphics.clone(), &source_path, label_hint, None).await?; } else { log::warn!( "Material override for '{}' references unsupported resource {:?}", @@ -384,6 +384,12 @@ pub struct SerializedMeshRenderer { #[serde(default)] pub material_customisation: Vec<SerializedMaterialcustomisation>, + + #[serde(default)] + #[serde(alias = "custom_import_scale")] + #[serde(alias = "editor_import_scale")] + #[serde(alias = "baked_import_scale")] + pub import_scale: Option<f32>, } #[derive(Default, Debug, Clone, Serialize, Deserialize)] @@ -470,6 +476,7 @@ impl SerializedMeshRenderer { handle: handle.path.clone(), material_override: renderer.material_overrides.clone(), material_customisation, + import_scale: Some(renderer.import_scale()), } } } @@ -1147,17 +1147,21 @@ impl InspectableComponent for MeshRenderer { if let ResourceReferenceType::File(uri) = &model_reference.ref_type { if is_probably_model_uri(uri) { - let mut import_scale = ASSET_REGISTRY.model_import_scale(&model_reference); + let mut import_scale = self.import_scale(); ui.horizontal(|ui| { - ui.label("Imported Scale"); + ui.label("Import Scale"); let resp = ui.add( egui::DragValue::new(&mut import_scale) .speed(0.01) .range(0.0001..=10_000.0), ); - if resp.drag_stopped() || (resp.changed() && resp.lost_focus()) { - *signal = Signal::SetModelImportScale(*entity, import_scale); + if resp.changed() { + self.set_import_scale(import_scale); + } + + if ui.button("Reset").clicked() { + self.set_import_scale(1.0); } }); ui.add_space(6.0); @@ -1535,9 +1535,9 @@ pub enum Signal { /// Sets a material tint colour (RGBA, unmultiplied). SetMaterialTint(hecs::Entity, String, [f32; 4]), - /// Sets the import scale for the currently assigned model on an entity's MeshRenderer. + /// Sets (bakes) the import scale for an entity's MeshRenderer. /// - /// This is an asset-level setting keyed by the model's resource reference. + /// This updates the MeshRenderer's baked import scale (saved into the scene and used at runtime). SetModelImportScale(hecs::Entity, f32), RequestNewWindow(WindowData), } @@ -52,10 +52,8 @@ impl SignalController for Editor { } Signal::SetModelImportScale(entity, scale) => { - if let Ok(renderer) = self.world.get::<&MeshRenderer>(*entity) { - let reference = renderer.model().path.clone(); - dropbear_engine::asset::ASSET_REGISTRY - .set_model_import_scale(reference.clone(), *scale); + if let Ok(mut renderer) = self.world.get::<&mut MeshRenderer>(*entity) { + renderer.set_import_scale(*scale); } self.signal = Signal::None; Ok(()) @@ -656,7 +654,7 @@ impl SignalController for Editor { PathBuf::from(&uri_clone) }; - Model::load(graphics_clone.clone(), &path, Some(&uri_clone)).await? + Model::load(graphics_clone.clone(), &path, Some(&uri_clone), None).await? }; // Ensure imports start as pure white; users can tint later. @@ -710,6 +708,7 @@ impl SignalController for Editor { graphics_clone.clone(), &path, Some(&uri_clone), + None, ) .await?; @@ -268,6 +268,8 @@ async fn load_renderer_from_serialized( uri.ends_with("internal/dropbear/models/cube") } + let import_scale = renderer.import_scale.unwrap_or(1.0); + let mut mesh_renderer = match &renderer.handle.ref_type { ResourceReferenceType::None => anyhow::bail!( "Renderer for '{}' does not specify an asset reference", @@ -283,7 +285,7 @@ async fn load_renderer_from_serialized( }); let loaded = LoadedModel::new_raw(&ASSET_REGISTRY, model); - MeshRenderer::from_handle(loaded) + MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } ResourceReferenceType::File(reference) => { if is_legacy_internal_cube_uri(reference) { @@ -300,13 +302,15 @@ async fn load_renderer_from_serialized( MeshRenderer::from_handle(loaded_model) } else { let path = renderer.handle.resolve()?; - MeshRenderer::from_path(graphics.clone(), &path, Some(&label)).await? + let loaded = Model::load(graphics.clone(), &path, Some(&label), None).await?; + MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } } ResourceReferenceType::Bytes(bytes) => { - let model = - Model::load_from_memory(graphics.clone(), bytes.clone(), Some(&label)).await?; - MeshRenderer::from_handle(model) + let loaded = + Model::load_from_memory(graphics.clone(), bytes.clone(), Some(&label), None) + .await?; + MeshRenderer::from_handle_with_import_scale(loaded, import_scale) } ResourceReferenceType::Cuboid { size_bits } => { let size = [ @@ -323,7 +327,7 @@ async fn load_renderer_from_serialized( loaded_model.refresh_registry(); - MeshRenderer::from_handle(loaded_model) + MeshRenderer::from_handle_with_import_scale(loaded_model, import_scale) } }; @@ -338,7 +342,7 @@ async fn load_renderer_from_serialized( ) { let source_path = override_entry.source_model.resolve()?; let label_hint = override_entry.source_model.as_uri(); - if let Err(err) = Model::load(graphics.clone(), &source_path, label_hint).await { + if let Err(err) = Model::load(graphics.clone(), &source_path, label_hint, None).await { log::warn!( "Failed to preload source model {:?} for override '{}': {}", override_entry.source_model, @@ -432,5 +436,7 @@ async fn load_renderer_from_serialized( mesh_renderer.sync_asset_registry(); } + mesh_renderer.set_import_scale(import_scale); + Ok(mesh_renderer) } @@ -271,21 +271,30 @@ impl Scene for Runtime { lights }; - let renderers = { - let mut renderers = Vec::new(); - let mut query = self.world.query::<&MeshRenderer>(); - for (_, renderer) in query.iter() { - renderers.push(renderer.clone()); - } - renderers - }; - let mut model_batches: HashMap<ModelId, Vec<InstanceRaw>> = HashMap::new(); - for renderer in &renderers { - model_batches - .entry(renderer.model_id()) - .or_default() - .push(renderer.instance.to_raw()); + { + let mut query = self.world.query::<( + &mut MeshRenderer, + Option<&dropbear_engine::entity::Transform>, + Option<&dropbear_engine::entity::EntityTransform>, + )>(); + + for (_, (renderer, transform_opt, entity_transform_opt)) in query.iter() { + let transform = if let Some(entity_transform) = entity_transform_opt { + entity_transform.sync() + } else if let Some(transform) = transform_opt { + *transform + } else { + continue; + }; + + renderer.update(&transform); + + model_batches + .entry(renderer.model_id()) + .or_default() + .push(renderer.instance.to_raw()); + } } let mut prepared_models = Vec::new();