diff --git a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs index a500bc48e..f0193d0d0 100644 --- a/Source/Editor/Content/Proxy/SkinnedModelProxy.cs +++ b/Source/Editor/Content/Proxy/SkinnedModelProxy.cs @@ -59,7 +59,10 @@ namespace FlaxEditor.Content // Check if asset is streamed enough var asset = (SkinnedModel)request.Asset; - return asset.LoadedLODs >= Mathf.Max(1, (int)(asset.LODs.Length * ThumbnailsModule.MinimumRequiredResourcesQuality)); + var lods = asset.LODs.Length; + if (asset.IsLoaded && lods == 0) + return true; // Skeleton-only model + return asset.LoadedLODs >= Mathf.Max(1, (int)(lods * ThumbnailsModule.MinimumRequiredResourcesQuality)); } /// diff --git a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs index a5354a802..2310668b6 100644 --- a/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs +++ b/Source/Editor/Viewport/Previews/AnimatedModelPreview.cs @@ -273,6 +273,41 @@ namespace FlaxEditor.Viewport.Previews _playAnimationOnce = true; } + /// + /// Gets the skinned model bounds. Handles skeleton-only assets. + /// + /// The local bounds. + public BoundingBox GetBounds() + { + var box = BoundingBox.Zero; + var skinnedModel = SkinnedModel; + if (skinnedModel && skinnedModel.IsLoaded) + { + if (skinnedModel.LODs.Length != 0) + { + // Use model geometry bounds + box = skinnedModel.GetBox(); + } + else + { + // Use skeleton bounds + _previewModel.GetCurrentPose(out var pose); + if (pose != null && pose.Length != 0) + { + var point = pose[0].TranslationVector; + box = new BoundingBox(point, point); + for (int i = 1; i < pose.Length; i++) + { + point = pose[i].TranslationVector; + box.Minimum = Vector3.Min(box.Minimum, point); + box.Maximum = Vector3.Max(box.Maximum, point); + } + } + } + } + return box; + } + private void OnBegin(RenderTask task, GPUContext context) { if (!ScaleToFit) @@ -290,7 +325,7 @@ namespace FlaxEditor.Viewport.Previews if (skinnedModel && skinnedModel.IsLoaded) { float targetSize = 50.0f; - BoundingBox box = skinnedModel.GetBox(); + BoundingBox box = GetBounds(); float maxSize = Mathf.Max(0.001f, (float)box.Size.MaxValue); float scale = targetSize / maxSize; _previewModel.Scale = new Vector3(scale); @@ -411,13 +446,21 @@ namespace FlaxEditor.Viewport.Previews base.Draw(); var skinnedModel = _previewModel.SkinnedModel; - if (_showCurrentLOD && skinnedModel) + if (skinnedModel == null || !skinnedModel.IsLoaded) + return; + var lods = skinnedModel.LODs; + if (lods.Length == 0) + { + // Force show skeleton for models without geometry + ShowNodes = true; + return; + } + if (_showCurrentLOD) { var lodIndex = ComputeLODIndex(skinnedModel); string text = string.Format("Current LOD: {0}", lodIndex); if (lodIndex != -1) { - var lods = skinnedModel.LODs; lodIndex = Mathf.Clamp(lodIndex + PreviewActor.LODBias, 0, lods.Length - 1); var lod = lods[lodIndex]; int triangleCount = 0, vertexCount = 0; diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs index d5551883a..7660c6c8e 100644 --- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs +++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs @@ -712,21 +712,24 @@ namespace FlaxEditor.Windows.Assets Render2D.PushClip(new Rectangle(Float2.Zero, size)); var meshDatas = Proxy.Window._meshDatas; - var lodIndex = Mathf.Clamp(_lod, 0, meshDatas.Length - 1); - var lod = meshDatas[lodIndex]; - var mesh = Mathf.Clamp(_mesh, -1, lod.Length - 1); - if (mesh == -1) + if (meshDatas.Length != 0) { - for (int meshIndex = 0; meshIndex < lod.Length; meshIndex++) + var lodIndex = Mathf.Clamp(_lod, 0, meshDatas.Length - 1); + var lod = meshDatas[lodIndex]; + var mesh = Mathf.Clamp(_mesh, -1, lod.Length - 1); + if (mesh == -1) { - if (_isolateIndex != -1 && _isolateIndex != meshIndex) - continue; - DrawMeshUVs(meshIndex, lod[meshIndex]); + for (int meshIndex = 0; meshIndex < lod.Length; meshIndex++) + { + if (_isolateIndex != -1 && _isolateIndex != meshIndex) + continue; + DrawMeshUVs(meshIndex, lod[meshIndex]); + } + } + else + { + DrawMeshUVs(mesh, lod[mesh]); } - } - else - { - DrawMeshUVs(mesh, lod[mesh]); } Render2D.PopClip(); @@ -1314,7 +1317,7 @@ namespace FlaxEditor.Windows.Assets protected override void OnAssetLoaded() { _refreshOnLODsLoaded = true; - _preview.ViewportCamera.SetArcBallView(Asset.GetBox()); + _preview.ViewportCamera.SetArcBallView(_preview.GetBounds()); UpdateEffectsOnAsset(); // TODO: disable streaming for this model diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp index 6476718b5..a2e687131 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.cpp +++ b/Source/Engine/Content/Assets/SkinnedModel.cpp @@ -292,21 +292,29 @@ SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source) bool SkinnedModel::Intersects(const Ray& ray, const Matrix& world, Real& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex) { + if (LODs.Count() == 0) + return false; return LODs[lodIndex].Intersects(ray, world, distance, normal, mesh); } bool SkinnedModel::Intersects(const Ray& ray, const Transform& transform, Real& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex) { + if (LODs.Count() == 0) + return false; return LODs[lodIndex].Intersects(ray, transform, distance, normal, mesh); } BoundingBox SkinnedModel::GetBox(const Matrix& world, int32 lodIndex) const { + if (LODs.Count() == 0) + return BoundingBox::Zero; return LODs[lodIndex].GetBox(world); } BoundingBox SkinnedModel::GetBox(int32 lodIndex) const { + if (LODs.Count() == 0) + return BoundingBox::Zero; return LODs[lodIndex].GetBox(); } @@ -837,6 +845,7 @@ bool SkinnedModel::Init(const Span& meshesCountPerLod) for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++) LODs[lodIndex].Dispose(); LODs.Resize(meshesCountPerLod.Length()); + _initialized = true; // Setup meshes for (int32 lodIndex = 0; lodIndex < meshesCountPerLod.Length(); lodIndex++) @@ -1069,7 +1078,7 @@ Asset::LoadResult SkinnedModel::load() // Amount of material slots int32 materialSlotsCount; stream->ReadInt32(&materialSlotsCount); - if (materialSlotsCount <= 0 || materialSlotsCount > 4096) + if (materialSlotsCount < 0 || materialSlotsCount > 4096) return LoadResult::InvalidData; MaterialSlots.Resize(materialSlotsCount, false); @@ -1093,9 +1102,10 @@ Asset::LoadResult SkinnedModel::load() // Amount of LODs byte lods; stream->ReadByte(&lods); - if (lods == 0 || lods > MODEL_MAX_LODS) + if (lods > MODEL_MAX_LODS) return LoadResult::InvalidData; LODs.Resize(lods); + _initialized = true; // For each LOD for (int32 lodIndex = 0; lodIndex < lods; lodIndex++) @@ -1220,6 +1230,7 @@ void SkinnedModel::unload(bool isReloading) LODs[i].Dispose(); LODs.Clear(); Skeleton.Dispose(); + _initialized = false; _loadedLODs = 0; _skeletonRetargets.Clear(); ClearSkeletonMapping(); diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h index 6e40e9665..11fc0c0aa 100644 --- a/Source/Engine/Content/Assets/SkinnedModel.h +++ b/Source/Engine/Content/Assets/SkinnedModel.h @@ -36,6 +36,7 @@ private: Span NodesMapping; }; + bool _initialized = false; int32 _loadedLODs = 0; StreamSkinnedModelLODTask* _streamingTask = nullptr; Dictionary _skeletonMappingCache; @@ -63,7 +64,7 @@ public: /// FORCE_INLINE bool IsInitialized() const { - return LODs.HasItems(); + return _initialized; } /// diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp index 591d328b1..afa591e01 100644 --- a/Source/Engine/Graphics/Models/ModelData.cpp +++ b/Source/Engine/Graphics/Models/ModelData.cpp @@ -766,21 +766,11 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const return true; } const int32 lodCount = GetLODsCount(); - if (lodCount == 0 || lodCount > MODEL_MAX_LODS) + if (lodCount > MODEL_MAX_LODS) { Log::ArgumentOutOfRangeException(); return true; } - if (Materials.IsEmpty()) - { - Log::ArgumentOutOfRangeException(TEXT("MaterialSlots"), TEXT("Material slots collection cannot be empty.")); - return true; - } - if (!HasSkeleton()) - { - Log::InvalidOperationException(TEXT("Missing skeleton.")); - return true; - } // Version stream->WriteByte(1); diff --git a/Source/Engine/Tools/ModelTool/ModelTool.cpp b/Source/Engine/Tools/ModelTool/ModelTool.cpp index 103d2187a..e7849f52f 100644 --- a/Source/Engine/Tools/ModelTool/ModelTool.cpp +++ b/Source/Engine/Tools/ModelTool/ModelTool.cpp @@ -673,6 +673,15 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } case ModelType::SkinnedModel: { + // Add single node if imported skeleton is empty + if (data.Skeleton.Nodes.IsEmpty()) + { + data.Skeleton.Nodes.Resize(1); + data.Skeleton.Nodes[0].Name = TEXT("Root"); + data.Skeleton.Nodes[0].LocalTransform = Transform::Identity; + data.Skeleton.Nodes[0].ParentIndex = -1; + } + // Special case if imported model has no bones but has valid skeleton and meshes. // We assume that every mesh uses a single bone. Copy nodes to bones. if (data.Skeleton.Bones.IsEmpty() && Math::IsInRange(data.Skeleton.Nodes.Count(), 1, MAX_BONES_PER_MODEL)) @@ -700,16 +709,6 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op } // Validate - if (data.LODs.IsEmpty() || data.LODs[0].Meshes.IsEmpty()) - { - errorMsg = TEXT("Imported model has no valid geometry."); - return true; - } - if (data.Skeleton.Nodes.IsEmpty() || data.Skeleton.Bones.IsEmpty()) - { - errorMsg = TEXT("Imported model has no skeleton."); - return true; - } if (data.Skeleton.Bones.Count() > MAX_BONES_PER_MODEL) { errorMsg = String::Format(TEXT("Imported model skeleton has too many bones. Imported: {0}, maximum supported: {1}. Please optimize your asset."), data.Skeleton.Bones.Count(), MAX_BONES_PER_MODEL); @@ -720,7 +719,8 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op LOG(Warning, "Imported skinned model has more than one LOD. Removing the lower LODs. Only single one is supported."); data.LODs.Resize(1); } - for (int32 i = 0; i < data.LODs[0].Meshes.Count(); i++) + const int32 meshesCount = data.LODs.Count() != 0 ? data.LODs[0].Meshes.Count() : 0; + for (int32 i = 0; i < meshesCount; i++) { const auto mesh = data.LODs[0].Meshes[i]; if (mesh->BlendIndices.IsEmpty() || mesh->BlendWeights.IsEmpty()) @@ -771,7 +771,7 @@ bool ModelTool::ImportModel(const String& path, ModelData& meshData, Options& op #endif } - LOG(Info, "Imported skeleton has {0} bones, {3} nodes, {1} meshes and {2} material", data.Skeleton.Bones.Count(), data.LODs[0].Meshes.Count(), data.Materials.Count(), data.Nodes.Count()); + LOG(Info, "Imported skeleton has {0} bones, {3} nodes, {1} meshes and {2} material", data.Skeleton.Bones.Count(), meshesCount, data.Materials.Count(), data.Nodes.Count()); break; } case ModelType::Animation: