ambient_model_import/
lib.rs

1use std::{f32::consts::PI, path::PathBuf, sync::Arc};
2
3use ambient_animation::AnimationOutputs;
4use ambient_core::{bounding::local_bounding_aabb, transform::translation};
5use ambient_editor_derive::ElementEditor;
6use ambient_renderer::materials::pbr_material::PbrMaterialDesc;
7use ambient_std::{
8    asset_cache::{AssetCache, SyncAssetKeyExt},
9    asset_url::AbsAssetUrl,
10    download_asset::AssetsCacheDir,
11};
12use anyhow::{anyhow, Context};
13use async_recursion::async_recursion;
14use futures::FutureExt;
15use glam::{Mat4, Vec3, Vec4};
16use image::RgbaImage;
17use model_crate::{ModelCrate, ModelNodeRef};
18use relative_path::RelativePathBuf;
19use serde::{Deserialize, Serialize};
20
21pub mod assimp;
22pub mod fbx;
23pub mod gltf;
24pub mod model_crate;
25
26pub type TextureResolver = Arc<dyn Fn(String) -> futures::future::BoxFuture<'static, Option<RgbaImage>> + Sync + Send>;
27
28#[derive(Default, Clone, Debug)]
29pub struct ModelImportPipeline {
30    pub steps: Vec<ModelImportTransform>,
31}
32impl ModelImportPipeline {
33    pub fn new() -> Self {
34        Self::default()
35    }
36    pub fn model(url: AbsAssetUrl) -> Self {
37        ModelImportPipeline::new().add_step(ModelImportTransform::ImportModelFromUrl { url, normalize: true, force_assimp: false })
38    }
39    pub fn model_raw(url: AbsAssetUrl) -> Self {
40        ModelImportPipeline::new().add_step(ModelImportTransform::ImportModelFromUrl { url, normalize: false, force_assimp: false })
41    }
42    pub fn add_step(mut self, step: ModelImportTransform) -> Self {
43        self.steps.push(step);
44        self
45    }
46    fn get_cache_path(&self) -> anyhow::Result<String> {
47        for step in &self.steps {
48            if let ModelImportTransform::ImportModelFromUrl { url, .. } = step {
49                return Ok(url.relative_cache_path());
50            } else if let ModelImportTransform::MergeMeshLods { lods, .. } = step {
51                return Ok(format!("merged_mesh_lods/{}", lods[0].get_cache_path().context("Lod 0 doesn't have a cache path")?));
52            } else if let ModelImportTransform::MergeUnityMeshLods { url, .. } = step {
53                return Ok(url.relative_cache_path());
54            }
55        }
56        Err(anyhow!("Can't create cache path, no ImportModelFromUrl or MergeMeshLods"))
57    }
58    pub async fn produce_crate(&self, assets: &AssetCache) -> anyhow::Result<ModelCrate> {
59        let mut asset_crate = ModelCrate::new();
60        for step in &self.steps {
61            step.run(assets, &mut asset_crate).await.with_context(|| format!("Failed to run step: {step:?}"))?;
62        }
63        Ok(asset_crate)
64    }
65    pub async fn produce_local_model_url(&self, asset_cache: &AssetCache) -> anyhow::Result<PathBuf> {
66        let cache_path = AssetsCacheDir.get(asset_cache).join("pipelines").join(self.get_cache_path()?);
67        let model_crate = self.clone().add_step(ModelImportTransform::Finalize).produce_crate(asset_cache).await?;
68        model_crate.produce_local_model_url(format!("{}/", cache_path.to_str().unwrap()).into()).await
69    }
70    // pub async fn produce_local_model(&self, asset_cache: &AssetCache) -> anyhow::Result<Model> {
71    //     let url = self.produce_local_model_url(asset_cache).await?;
72    //     let mut model = Model::from_file(&url).await?;
73    //     model.load(asset_cache).await?;
74    //     Ok(model)
75    // }
76}
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ElementEditor)]
79#[serde(tag = "type")]
80pub enum MaterialFilter {
81    /// Replace all materials.
82    All,
83    /// Replace all materials that match this name exactly.
84    ByName {
85        /// The material name to replace. Must match exactly (i.e. is case-sensitive and does not ignore whitespace).
86        name: String,
87    },
88}
89impl MaterialFilter {
90    pub fn by_name(name: impl Into<String>) -> Self {
91        Self::ByName { name: name.into() }
92    }
93    fn matches(&self, mat: &PbrMaterialDesc) -> bool {
94        match self {
95            MaterialFilter::All => true,
96            MaterialFilter::ByName { name } => mat.name.as_ref() == Some(name),
97        }
98    }
99    fn is_all(&self) -> bool {
100        matches!(self, MaterialFilter::All)
101    }
102}
103impl Default for MaterialFilter {
104    fn default() -> Self {
105        Self::All
106    }
107}
108
109#[derive(Clone, Debug)]
110pub enum ModelImportTransform {
111    ImportModelFromUrl { url: AbsAssetUrl, normalize: bool, force_assimp: bool },
112    MergeMeshLods { lods: Vec<ModelImportPipeline>, lod_cutoffs: Option<Vec<f32>> },
113    MergeUnityMeshLods { url: AbsAssetUrl, lod_cutoffs: Option<Vec<f32>> },
114    SetName { name: String },
115    Transform(ModelTransform),
116    OverrideMaterial { filter: MaterialFilter, material: Box<PbrMaterialDesc> },
117    CapTextureSizes { max_size: ModelTextureSize },
118    // RemoveAllMaterials,
119    // SetAnimatable { animatable: bool },
120    CreatePrefab,
121    CreateColliderFromModel,
122    CreateCharacterCollider,
123    Finalize,
124}
125impl ModelImportTransform {
126    #[async_recursion]
127    pub async fn run(&self, assets: &AssetCache, model_crate: &mut ModelCrate) -> anyhow::Result<()> {
128        match self {
129            ModelImportTransform::ImportModelFromUrl { url, normalize, force_assimp } => {
130                model_crate.import(assets, url, *normalize, *force_assimp, Arc::new(|_| async move { None }.boxed())).await?;
131            }
132            ModelImportTransform::MergeMeshLods { lods, lod_cutoffs } => {
133                let mut res_lods = Vec::new();
134                for lod in lods {
135                    res_lods.push(lod.produce_crate(assets).await?);
136                }
137                model_crate
138                    .merge_mesh_lods(lod_cutoffs.clone(), res_lods.iter().map(|lod| ModelNodeRef { model: lod, root: None }).collect());
139            }
140            ModelImportTransform::MergeUnityMeshLods { url, lod_cutoffs } => {
141                let source = ModelImportPipeline::model(url.clone()).produce_crate(assets).await?;
142                model_crate.merge_unity_style_mesh_lods(&source, lod_cutoffs.clone());
143            }
144            ModelImportTransform::SetName { name } => {
145                model_crate.model_world_mut().add_resource(ambient_core::name(), name.clone());
146            }
147            ModelImportTransform::Transform(transform) => transform.apply(model_crate),
148            ModelImportTransform::OverrideMaterial { filter, material } => {
149                model_crate.override_material(filter, (**material).clone());
150            }
151            ModelImportTransform::CapTextureSizes { max_size } => {
152                model_crate.cap_texture_sizes(max_size.size());
153            }
154            // AssetTransform::RemoveAllMaterials => {
155            //     model.cpu_materials.clear();
156            //     model.gpu_materials.clear();
157            // }
158            // AssetTransform::SetAnimatable { animatable } => {
159            //     model.animatable = Some(*animatable);
160            // }
161            ModelImportTransform::CreatePrefab => {
162                model_crate.create_prefab_from_model();
163            }
164            ModelImportTransform::CreateColliderFromModel => {
165                model_crate.create_collider_from_model(assets, false, true)?;
166            }
167            ModelImportTransform::CreateCharacterCollider => {
168                model_crate.create_character_collider(None, None);
169            }
170            ModelImportTransform::Finalize => {
171                model_crate.finalize_model();
172            }
173        }
174        Ok(())
175    }
176}
177
178#[derive(Clone, Debug, Serialize, Deserialize)]
179#[serde(tag = "type")]
180pub enum ModelTransform {
181    /// Rotate Y up to Z up.
182    RotateYUpToZUp,
183    /// Rotate X by `deg` degrees.
184    RotateX {
185        /// The degrees to rotate this model around the X axis.
186        deg: f32,
187    },
188    /// Rotate Y by `deg` degrees.
189    RotateY {
190        /// The degrees to rotate this model around the Y axis.
191        deg: f32,
192    },
193    /// Rotate Z by `deg` degrees.
194    RotateZ {
195        /// The degrees to rotate this model around the Z axis.
196        deg: f32,
197    },
198    /// Scale this model.
199    Scale {
200        /// The factor to scale this model by.
201        scale: f32,
202    },
203    /// Translate this model.
204    Translate {
205        /// The translation to apply to this model (i.e. this model will be moved by `translation` in the current coordinate space).
206        translation: Vec3,
207    },
208    /// Scale this model's AABB.
209    ScaleAABB {
210        /// The factor to scale this model's AABB by.
211        scale: f32,
212    },
213    /// Scale this model's animations (spatially, not in time).
214    ScaleAnimations {
215        /// The factor to scale this model's animations by.
216        scale: f32,
217    },
218    /// Re-root this mesh.
219    SetRoot {
220        /// The name of the node to set as the new root for this mesh.
221        name: String,
222    },
223    /// Re-center this mesh such that the root is located at the origin.
224    Center,
225}
226impl ModelTransform {
227    pub fn apply(&self, model_crate: &mut ModelCrate) {
228        match self {
229            ModelTransform::RotateYUpToZUp => {
230                let transform = Mat4::from_cols(Vec4::X, Vec4::Z, Vec4::Y, Vec4::W);
231                model_crate.model_mut().transform(transform);
232            }
233            ModelTransform::RotateX { deg } => {
234                model_crate.model_mut().transform(Mat4::from_rotation_x(deg * PI / 180.));
235            }
236            ModelTransform::RotateY { deg } => {
237                model_crate.model_mut().transform(Mat4::from_rotation_y(deg * PI / 180.));
238            }
239            ModelTransform::RotateZ { deg } => {
240                model_crate.model_mut().transform(Mat4::from_rotation_z(deg * PI / 180.));
241            }
242            ModelTransform::Scale { scale } => {
243                model_crate.model_mut().transform(Mat4::from_scale(Vec3::ONE * *scale));
244            }
245            ModelTransform::Translate { translation } => {
246                model_crate.model_mut().transform(Mat4::from_translation(*translation));
247            }
248            ModelTransform::ScaleAABB { scale } => {
249                let world = model_crate.model_world_mut();
250                let aabb = world.resource_mut(local_bounding_aabb());
251                aabb.min *= *scale;
252                aabb.max *= *scale;
253            }
254            ModelTransform::ScaleAnimations { scale: anim_scale } => {
255                for clip in model_crate.animations.content.values_mut() {
256                    *clip = clip.map_outputs(|outputs| {
257                        if outputs.component() == translation() {
258                            match outputs {
259                                AnimationOutputs::Vec3 { component, data } => {
260                                    AnimationOutputs::Vec3 { component: *component, data: data.iter().map(|x| *x * *anim_scale).collect() }
261                                }
262                                AnimationOutputs::Quat { component: _, data: _ } => unreachable!(),
263                                AnimationOutputs::Vec3Field { component, field, data } => AnimationOutputs::Vec3Field {
264                                    component: *component,
265                                    field: *field,
266                                    data: data.iter().map(|x| *x * *anim_scale).collect(),
267                                },
268                            }
269                        } else {
270                            outputs.clone()
271                        }
272                    });
273                }
274            }
275            ModelTransform::SetRoot { name } => {
276                if let Some(id) = model_crate.model().get_entity_id_by_name(name) {
277                    model_crate.make_new_root(id);
278                }
279            }
280            ModelTransform::Center => {
281                model_crate.model_mut().center();
282            }
283        }
284    }
285}
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, ElementEditor)]
288pub enum ModelTextureSize {
289    /// Cap this model's textures to 128x128.
290    X128,
291    /// Cap this model's textures to 256x256.
292    X256,
293    /// Cap this model's textures to 512x512.
294    X512,
295    /// Cap this model's textures to 1024x1024.
296    X1024,
297    /// Cap this model's textures to 2048x2048.
298    X2048,
299    /// Cap this model's textures to 4096x4096.
300    X4096,
301    /// Cap this model's textures to SIZE x SIZE.
302    /// It is strongly recommended that this is a power of two.
303    Custom(u32),
304}
305impl ModelTextureSize {
306    pub fn size(&self) -> u32 {
307        match self {
308            ModelTextureSize::X128 => 128,
309            ModelTextureSize::X256 => 256,
310            ModelTextureSize::X512 => 512,
311            ModelTextureSize::X1024 => 1024,
312            ModelTextureSize::X2048 => 2048,
313            ModelTextureSize::X4096 => 4096,
314            ModelTextureSize::Custom(size) => *size,
315        }
316    }
317}
318impl Default for ModelTextureSize {
319    fn default() -> Self {
320        Self::X512
321    }
322}
323
324// #[derive(Debug, Clone)]
325// pub struct ModelFromAssetPipeline(pub ModelImportPipeline);
326// impl ModelFromAssetPipeline {
327//     pub fn gltf_file(file: &str) -> Self {
328//         Self(ModelImportPipeline::new().add_step(ModelImportTransform::ImportModelFromUrl {
329//             url: file.to_string(),
330//             normalize: true,
331//             force_assimp: false,
332//         }))
333//     }
334// }
335// #[async_trait]
336// impl AsyncAssetKey<AssetResult<Arc<Model>>> for ModelFromAssetPipeline {
337//     async fn load(self, assets: AssetCache) -> AssetResult<Arc<Model>> {
338//         Ok(Arc::new(self.0.produce_local_model(&assets).await?))
339//     }
340// }
341
342pub const MODEL_EXTENSIONS: &[&str] = &["glb", "fbx", "obj"];
343
344/// ../[path]
345pub fn dotdot_path(path: impl Into<RelativePathBuf>) -> RelativePathBuf {
346    RelativePathBuf::from("..").join(path.into())
347}
348pub trait RelativePathBufExt {
349    /// [prefix]/[self]
350    fn prejoin(&self, prefix: impl Into<RelativePathBuf>) -> Self;
351}
352impl RelativePathBufExt for RelativePathBuf {
353    fn prejoin(&self, prefix: impl Into<RelativePathBuf>) -> Self {
354        let prefix: RelativePathBuf = prefix.into();
355        prefix.join(self)
356    }
357}