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 }
77
78#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ElementEditor)]
79#[serde(tag = "type")]
80pub enum MaterialFilter {
81 All,
83 ByName {
85 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 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 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 RotateYUpToZUp,
183 RotateX {
185 deg: f32,
187 },
188 RotateY {
190 deg: f32,
192 },
193 RotateZ {
195 deg: f32,
197 },
198 Scale {
200 scale: f32,
202 },
203 Translate {
205 translation: Vec3,
207 },
208 ScaleAABB {
210 scale: f32,
212 },
213 ScaleAnimations {
215 scale: f32,
217 },
218 SetRoot {
220 name: String,
222 },
223 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 X128,
291 X256,
293 X512,
295 X1024,
297 X2048,
299 X4096,
301 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
324pub const MODEL_EXTENSIONS: &[&str] = &["glb", "fbx", "obj"];
343
344pub fn dotdot_path(path: impl Into<RelativePathBuf>) -> RelativePathBuf {
346 RelativePathBuf::from("..").join(path.into())
347}
348pub trait RelativePathBufExt {
349 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}