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: