diff --git a/Flax.sln.DotSettings b/Flax.sln.DotSettings
index a270ffa47..e71c2da2d 100644
--- a/Flax.sln.DotSettings
+++ b/Flax.sln.DotSettings
@@ -321,6 +321,9 @@
True
True
True
+ True
+ True
+ True
True
True
True
diff --git a/Source/Editor/Windows/Assets/ModelBaseWindow.cs b/Source/Editor/Windows/Assets/ModelBaseWindow.cs
index c4ec80e44..5fd90bd23 100644
--- a/Source/Editor/Windows/Assets/ModelBaseWindow.cs
+++ b/Source/Editor/Windows/Assets/ModelBaseWindow.cs
@@ -36,6 +36,10 @@ namespace FlaxEditor.Windows.Assets
Asset = window.Asset;
}
+ public virtual void OnSave()
+ {
+ }
+
public virtual void OnClean()
{
Window = null;
@@ -109,7 +113,7 @@ namespace FlaxEditor.Windows.Assets
{
AnchorPreset = AnchorPresets.StretchAll,
Offsets = new Margin(0, 0, _toolstrip.Bottom, 0),
- SplitterValue = 0.65f,
+ SplitterValue = 0.59f,
Parent = this
};
@@ -139,9 +143,7 @@ namespace FlaxEditor.Windows.Assets
foreach (var child in _tabs.Children)
{
if (child is Tab tab && tab.Proxy.Window != null)
- {
tab.Proxy.OnClean();
- }
}
base.UnlinkItem();
@@ -199,7 +201,7 @@ namespace FlaxEditor.Windows.Assets
///
public override void OnLayoutDeserialize()
{
- _split.SplitterValue = 0.65f;
+ _split.SplitterValue = 0.59f;
}
}
}
diff --git a/Source/Editor/Windows/Assets/ModelWindow.cs b/Source/Editor/Windows/Assets/ModelWindow.cs
index af3bfdf22..726d6bcf9 100644
--- a/Source/Editor/Windows/Assets/ModelWindow.cs
+++ b/Source/Editor/Windows/Assets/ModelWindow.cs
@@ -876,10 +876,13 @@ namespace FlaxEditor.Windows.Assets
{
if (!IsEdited)
return;
-
if (_asset.WaitForLoaded())
- {
return;
+
+ foreach (var child in _tabs.Children)
+ {
+ if (child is Tab tab && tab.Proxy.Window != null)
+ tab.Proxy.OnSave();
}
if (_asset.Save())
diff --git a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
index 6e00558e8..67440634b 100644
--- a/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
+++ b/Source/Editor/Windows/Assets/SkinnedModelWindow.cs
@@ -2,6 +2,8 @@
using System;
using System.Collections.Generic;
+using System.IO;
+using System.Linq;
using System.Reflection;
using System.Threading;
using System.Threading.Tasks;
@@ -356,15 +358,12 @@ namespace FlaxEditor.Windows.Assets
private void OnTreeNodeRightClick(TreeNode node, Float2 location)
{
- var menu = new ContextMenu
- {
- MinimumWidth = 120
- };
-
+ var menu = new ContextMenu();
+
var b = menu.AddButton("Copy name");
b.Tag = node.Text;
b.ButtonClicked += OnTreeNodeCopyName;
-
+
menu.Show(node, location);
}
@@ -750,6 +749,230 @@ namespace FlaxEditor.Windows.Assets
}
}
+ [CustomEditor(typeof(ProxyEditor))]
+ private sealed class RetargetPropertiesProxy : PropertiesProxyBase
+ {
+ internal class SetupProxy
+ {
+ public SkinnedModel Skeleton;
+ public Dictionary NodesMapping;
+ }
+
+ internal Dictionary Setups;
+
+ public override void OnSave()
+ {
+ base.OnSave();
+
+ if (Setups != null)
+ {
+ var retargetSetups = new SkinnedModel.SkeletonRetarget[Setups.Count];
+ int i = 0;
+ foreach (var setup in Setups)
+ {
+ retargetSetups[i++] = new SkinnedModel.SkeletonRetarget
+ {
+ SourceAsset = setup.Key?.ID ?? Guid.Empty,
+ SkeletonAsset = setup.Value.Skeleton?.ID ?? Guid.Empty,
+ NodesMapping = setup.Value.NodesMapping,
+ };
+ }
+ Window.Asset.SkeletonRetargets = retargetSetups;
+ }
+ }
+
+ private class ProxyEditor : ProxyEditorBase
+ {
+ public override void Initialize(LayoutElementsContainer layout)
+ {
+ var proxy = (RetargetPropertiesProxy)Values[0];
+ if (proxy.Asset == null || !proxy.Asset.IsLoaded)
+ {
+ layout.Label("Loading...");
+ return;
+ }
+ if (proxy.Setups == null)
+ {
+ proxy.Setups = new Dictionary();
+ var retargetSetups = proxy.Asset.SkeletonRetargets;
+ foreach (var retargetSetup in retargetSetups)
+ {
+ var sourceAsset = FlaxEngine.Content.LoadAsync(retargetSetup.SourceAsset);
+ if (sourceAsset)
+ {
+ proxy.Setups.Add(sourceAsset, new SetupProxy
+ {
+ Skeleton = FlaxEngine.Content.LoadAsync(retargetSetup.SkeletonAsset),
+ NodesMapping = retargetSetup.NodesMapping,
+ });
+ }
+ }
+ }
+ var targetNodes = proxy.Asset.Nodes;
+
+ layout.Space(10.0f);
+ var infoLabel = layout.Label("Each retarget setup defines how to convert animated skeleton pose from a source asset to this skinned model skeleton. It allows to play animation from different skeleton on this skeleton. See documentation to learn more.").Label;
+ infoLabel.Wrapping = TextWrapping.WrapWords;
+ infoLabel.AutoHeight = true;
+ layout.Space(10.0f);
+
+ // New setup
+ {
+ var setupGroup = layout.Group("New setup");
+ infoLabel = setupGroup.Label("Select model or animation asset to add new retarget source", TextAlignment.Center).Label;
+ infoLabel.Wrapping = TextWrapping.WrapWords;
+ infoLabel.AutoHeight = true;
+ var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl;
+ sourceAssetPicker.Height = 48;
+ sourceAssetPicker.CheckValid = CheckSourceAssetValid;
+ sourceAssetPicker.SelectedItemChanged += () =>
+ {
+ proxy.Setups.Add(sourceAssetPicker.SelectedAsset, new SetupProxy());
+ proxy.Window.MarkAsEdited();
+ RebuildLayout();
+ };
+ }
+
+ // Setups
+ foreach (var setup in proxy.Setups)
+ {
+ var sourceAsset = setup.Key;
+ if (sourceAsset == null)
+ continue;
+ var setupGroup = layout.Group(Path.GetFileNameWithoutExtension(sourceAsset.Path));
+ var settingsButton = setupGroup.AddSettingsButton();
+ settingsButton.Tag = sourceAsset;
+ settingsButton.Clicked += OnShowSetupSettings;
+
+ // Source asset picker
+ var sourceAssetPicker = setupGroup.AddPropertyItem("Source Asset").Custom().CustomControl;
+ sourceAssetPicker.SelectedAsset = sourceAsset;
+ sourceAssetPicker.CanEdit = false;
+ sourceAssetPicker.Height = 48;
+
+ if (sourceAsset is SkinnedModel sourceModel)
+ {
+ // Initialize nodes mapping structure
+ if (sourceModel.WaitForLoaded())
+ continue;
+ var sourceNodes = sourceModel.Nodes;
+ if (setup.Value.NodesMapping == null)
+ setup.Value.NodesMapping = new Dictionary();
+ var nodesMapping = setup.Value.NodesMapping;
+ foreach (var targetNode in targetNodes)
+ {
+ if (!nodesMapping.ContainsKey(targetNode.Name))
+ {
+ var node = string.Empty;
+ foreach (var sourceNode in sourceNodes)
+ {
+ if (string.Equals(targetNode.Name, sourceNode.Name, StringComparison.OrdinalIgnoreCase))
+ {
+ node = sourceNode.Name;
+ break;
+ }
+ }
+ nodesMapping.Add(targetNode.Name, node);
+ }
+ }
+
+ // Build source skeleton nodes list (with hierarchy indentation)
+ var items = new string[sourceNodes.Length + 1];
+ items[0] = string.Empty;
+ for (int i = 0; i < sourceNodes.Length; i++)
+ items[i + 1] = sourceNodes[i].Name;
+
+ // Show combo boxes with this skeleton nodes to retarget from
+ foreach (var targetNode in targetNodes)
+ {
+ var nodeName = targetNode.Name;
+ var propertyName = nodeName;
+ var tmp = targetNode.ParentIndex;
+ while (tmp != -1)
+ {
+ tmp = targetNodes[tmp].ParentIndex;
+ propertyName = " " + propertyName;
+ }
+ var comboBox = setupGroup.AddPropertyItem(propertyName).Custom().CustomControl;
+ comboBox.AddItems(items);
+ comboBox.Tag = new KeyValuePair(nodeName, sourceAsset);
+ comboBox.SelectedItem = nodesMapping[nodeName];
+ if (comboBox.SelectedIndex == -1)
+ comboBox.SelectedIndex = 0; // Auto-select empty node
+ comboBox.SelectedIndexChanged += OnSelectedNodeChanged;
+ }
+ }
+ else if (sourceAsset is Animation sourceAnimation)
+ {
+ // Show skeleton asset picker
+ var sourceSkeletonPicker = setupGroup.AddPropertyItem("Skeleton", "Skinned model that contains a skeleton for this animation retargeting.").Custom().CustomControl;
+ sourceSkeletonPicker.AssetType = new ScriptType(typeof(SkinnedModel));
+ sourceSkeletonPicker.SelectedAsset = setup.Value.Skeleton;
+ sourceSkeletonPicker.Height = 48;
+ sourceSkeletonPicker.SelectedItemChanged += () =>
+ {
+ setup.Value.Skeleton = (SkinnedModel)sourceSkeletonPicker.SelectedAsset;
+ proxy.Window.MarkAsEdited();
+ };
+ }
+ }
+ }
+
+ private void OnSelectedNodeChanged(ComboBox comboBox)
+ {
+ var proxy = (RetargetPropertiesProxy)Values[0];
+ var sourceAsset = ((KeyValuePair)comboBox.Tag).Value;
+ var nodeMappingKey = ((KeyValuePair)comboBox.Tag).Key;
+ var nodeMappingValue = comboBox.SelectedItem;
+ // TODO: check for recursion in setup
+ proxy.Setups[sourceAsset].NodesMapping[nodeMappingKey] = nodeMappingValue;
+ proxy.Window.MarkAsEdited();
+ }
+
+ private void OnShowSetupSettings(Image settingsButton, MouseButton button)
+ {
+ if (button == MouseButton.Left)
+ {
+ var sourceAsset = (Asset)settingsButton.Tag;
+ var menu = new ContextMenu { Tag = sourceAsset };
+ menu.AddButton("Clear", OnClearSetup);
+ menu.AddButton("Remove", OnRemoveSetup).Icon = Editor.Instance.Icons.Cross12;
+ menu.Show(settingsButton, new Float2(0, settingsButton.Height));
+ }
+ }
+
+ private void OnClearSetup(ContextMenuButton button)
+ {
+ var proxy = (RetargetPropertiesProxy)Values[0];
+ var sourceAsset = (Asset)button.ParentContextMenu.Tag;
+ var setup = proxy.Setups[sourceAsset];
+ setup.Skeleton = null;
+ foreach (var e in setup.NodesMapping.Keys.ToArray())
+ setup.NodesMapping[e] = string.Empty;
+ proxy.Window.MarkAsEdited();
+ RebuildLayout();
+ }
+
+ private void OnRemoveSetup(ContextMenuButton button)
+ {
+ var proxy = (RetargetPropertiesProxy)Values[0];
+ var sourceAsset = (Asset)button.ParentContextMenu.Tag;
+ proxy.Setups.Remove(sourceAsset);
+ proxy.Window.MarkAsEdited();
+ RebuildLayout();
+ }
+
+ private bool CheckSourceAssetValid(ContentItem item)
+ {
+ var proxy = (RetargetPropertiesProxy)Values[0];
+ return item is BinaryAssetItem binaryItem &&
+ (binaryItem.Type == typeof(SkinnedModel) || binaryItem.Type == typeof(Animation)) &&
+ item != proxy.Window.Item &&
+ !proxy.Setups.ContainsKey(binaryItem.LoadAsync());
+ }
+ }
+ }
+
[CustomEditor(typeof(ProxyEditor))]
private sealed class ImportPropertiesProxy : PropertiesProxyBase
{
@@ -850,6 +1073,16 @@ namespace FlaxEditor.Windows.Assets
}
}
+ private class RetargetTab : Tab
+ {
+ public RetargetTab(SkinnedModelWindow window)
+ : base("Retarget", window)
+ {
+ Proxy = new RetargetPropertiesProxy();
+ Presenter.Select(Proxy);
+ }
+ }
+
private class ImportTab : Tab
{
public ImportTab(SkinnedModelWindow window)
@@ -897,6 +1130,7 @@ namespace FlaxEditor.Windows.Assets
_tabs.AddTab(new SkeletonTab(this));
_tabs.AddTab(new MaterialsTab(this));
_tabs.AddTab(new UVsTab(this));
+ _tabs.AddTab(new RetargetTab(this));
_tabs.AddTab(new ImportTab(this));
// Highlight actor (used to highlight selected material slot, see UpdateEffectsOnAsset)
@@ -1038,6 +1272,14 @@ namespace FlaxEditor.Windows.Assets
{
if (!IsEdited)
return;
+ if (_asset.WaitForLoaded())
+ return;
+
+ foreach (var child in _tabs.Children)
+ {
+ if (child is Tab tab && tab.Proxy.Window != null)
+ tab.Proxy.OnSave();
+ }
if (_asset.Save())
{
diff --git a/Source/Engine/Animations/Graph/AnimGraph.cpp b/Source/Engine/Animations/Graph/AnimGraph.cpp
index 3314aecab..f10275c6b 100644
--- a/Source/Engine/Animations/Graph/AnimGraph.cpp
+++ b/Source/Engine/Animations/Graph/AnimGraph.cpp
@@ -7,6 +7,8 @@
#include "Engine/Graphics/Models/SkeletonData.h"
#include "Engine/Scripting/Scripting.h"
+extern void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& sourceMapping, Transform& node, int32 i);
+
ThreadLocal AnimGraphExecutor::Context;
RootMotionData RootMotionData::Identity = { Vector3(0.0f), Quaternion(0.0f, 0.0f, 0.0f, 1.0f) };
@@ -315,27 +317,21 @@ void AnimGraphExecutor::Update(AnimGraphInstanceData& data, float dt)
targetNodes[i] = targetSkeleton.Nodes[i].LocalTransform;
// Use skeleton mapping
- const Span mapping = data.NodesSkeleton->GetSkeletonMapping(_graph.BaseModel);
- if (mapping.IsValid())
+ const SkinnedModel::SkeletonMapping mapping = data.NodesSkeleton->GetSkeletonMapping(_graph.BaseModel);
+ if (mapping.NodesMapping.IsValid())
{
const auto& sourceSkeleton = _graph.BaseModel->Skeleton;
Transform* sourceNodes = animResult->Nodes.Get();
for (int32 i = 0; i < retargetNodes.Nodes.Count(); i++)
{
- const auto& targetNode = targetSkeleton.Nodes[i];
- const int32 nodeToNode = mapping[i];
+ const int32 nodeToNode = mapping.NodesMapping[i];
if (nodeToNode != -1)
{
- // Map source skeleton node to the target skeleton (use ref pose difference)
- const auto& sourceNode = sourceSkeleton.Nodes[nodeToNode];
- Transform value = sourceNodes[nodeToNode];
- Transform sourceToTarget = targetNode.LocalTransform - sourceNode.LocalTransform;
- value.Translation += sourceToTarget.Translation;
- value.Scale *= sourceToTarget.Scale;
- //value.Orientation = sourceToTarget.Orientation * value.Orientation; // TODO: find out why this doesn't match referenced animation when played on that skeleton originally
- //value.Orientation.Normalize();
- targetNodes[i] = value;
+ Transform node = sourceNodes[nodeToNode];
+ RetargetSkeletonNode(sourceSkeleton, targetSkeleton, mapping, node, i);
+ targetNodes[i] = node;
}
+
}
}
diff --git a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
index bd6c74630..3be8e85da 100644
--- a/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
+++ b/Source/Engine/Animations/Graph/AnimGroup.Animation.cpp
@@ -21,6 +21,24 @@ namespace
}
}
+void RetargetSkeletonNode(const SkeletonData& sourceSkeleton, const SkeletonData& targetSkeleton, const SkinnedModel::SkeletonMapping& mapping, Transform& node, int32 i)
+{
+ const int32 nodeToNode = mapping.NodesMapping[i];
+ if (nodeToNode == -1)
+ return;
+
+ // Map source skeleton node to the target skeleton (use ref pose difference)
+ const auto& sourceNode = sourceSkeleton.Nodes[nodeToNode];
+ const auto& targetNode = targetSkeleton.Nodes[i];
+ Transform value = node;
+ const Transform sourceToTarget = targetNode.LocalTransform - sourceNode.LocalTransform;
+ value.Translation += sourceToTarget.Translation;
+ value.Scale *= sourceToTarget.Scale;
+ value.Orientation = sourceToTarget.Orientation * value.Orientation; // TODO: find out why this doesn't match referenced animation when played on that skeleton originally
+ value.Orientation.Normalize();
+ node = value;
+}
+
int32 AnimGraphExecutor::GetRootNodeIndex(Animation* anim)
{
// TODO: cache the root node index (use dictionary with Animation* -> int32 for fast lookups)
@@ -257,21 +275,33 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
}
}
- // Evaluate nodes animations
- const Span mapping = _graph.BaseModel->GetSkeletonMapping(anim);
- if (mapping.IsInvalid())
+ // Get skeleton nodes mapping descriptor
+ const SkinnedModel::SkeletonMapping mapping = _graph.BaseModel->GetSkeletonMapping(anim);
+ if (mapping.NodesMapping.IsInvalid())
return;
+
+ // Evaluate nodes animations
const bool weighted = weight < 1.0f;
+ const bool retarget = mapping.SourceSkeleton && mapping.SourceSkeleton != mapping.TargetSkeleton;
const auto emptyNodes = GetEmptyNodes();
+ SkinnedModel::SkeletonMapping sourceMapping;
+ if (retarget)
+ sourceMapping = _graph.BaseModel->GetSkeletonMapping(mapping.SourceSkeleton);
for (int32 i = 0; i < nodes->Nodes.Count(); i++)
{
- const int32 nodeToChannel = mapping[i];
+ const int32 nodeToChannel = mapping.NodesMapping[i];
Transform& dstNode = nodes->Nodes[i];
Transform srcNode = emptyNodes->Nodes[i];
if (nodeToChannel != -1)
{
// Calculate the animated node transformation
anim->Data.Channels[nodeToChannel].Evaluate(animPos, &srcNode, false);
+
+ // Optionally retarget animation into the skeleton used by the Anim Graph
+ if (retarget)
+ {
+ RetargetSkeletonNode(mapping.SourceSkeleton->Skeleton, mapping.TargetSkeleton->Skeleton, sourceMapping, srcNode, i);
+ }
}
// Blend node
@@ -307,7 +337,7 @@ void AnimGraphExecutor::ProcessAnimation(AnimGraphImpulse* nodes, AnimGraphNode*
Transform rootNode = emptyNodes->Nodes[rootNodeIndex];
RootMotionData& dstNode = nodes->RootMotion;
RootMotionData srcNode(rootNode);
- ExtractRootMotion(mapping, rootNodeIndex, anim, animPos, animPrevPos, rootNode, srcNode);
+ ExtractRootMotion(mapping.NodesMapping, rootNodeIndex, anim, animPos, animPrevPos, rootNode, srcNode);
// Blend root motion
if (mode == ProcessAnimationMode::BlendAdditive)
diff --git a/Source/Engine/Content/Assets/SkinnedModel.cpp b/Source/Engine/Content/Assets/SkinnedModel.cpp
index 25f95b658..6476718b5 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.cpp
+++ b/Source/Engine/Content/Assets/SkinnedModel.cpp
@@ -12,6 +12,7 @@
#include "Engine/Graphics/RenderTask.h"
#include "Engine/Graphics/Models/ModelInstanceEntry.h"
#include "Engine/Graphics/Models/Config.h"
+#include "Engine/Content/Content.h"
#include "Engine/Content/WeakAssetReference.h"
#include "Engine/Content/Factories/BinaryAssetFactory.h"
#include "Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h"
@@ -155,51 +156,117 @@ void SkinnedModel::GetLODData(int32 lodIndex, BytesContainer& data) const
GetChunkData(chunkIndex, data);
}
-Span SkinnedModel::GetSkeletonMapping(Asset* source)
+SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source)
{
+ SkeletonMapping mapping;
+ mapping.TargetSkeleton = this;
if (WaitForLoaded() || !source || source->WaitForLoaded())
- return Span();
+ return mapping;
ScopeLock lock(Locker);
- Span result;
- if (!_skeletonMappingCache.TryGet(source, result))
+ SkeletonMappingData mappingData;
+ if (!_skeletonMappingCache.TryGet(source, mappingData))
{
PROFILE_CPU();
// Initialize the mapping
const int32 nodesCount = Skeleton.Nodes.Count();
- result = Span((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount);
+ mappingData.NodesMapping = Span((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount);
for (int32 i = 0; i < nodesCount; i++)
- result[i] = -1;
+ mappingData.NodesMapping[i] = -1;
+ SkeletonRetarget* retarget = nullptr;
+ const Guid sourceId = source->GetID();
+ for (auto& e : _skeletonRetargets)
+ {
+ if (e.SourceAsset == sourceId)
+ {
+ retarget = &e;
+ break;
+ }
+ }
if (const auto* sourceAnim = Cast(source))
{
- // Map animation channels to the skeleton nodes (by name)
const auto& channels = sourceAnim->Data.Channels;
- for (int32 i = 0; i < channels.Count(); i++)
+ if (retarget && retarget->SkeletonAsset)
{
- const NodeAnimationData& nodeAnim = channels[i];
- for (int32 j = 0; j < nodesCount; j++)
+ // Map retarget skeleton nodes from animation channels
+ if (auto* skeleton = Content::Load(retarget->SkeletonAsset))
{
- if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), nodeAnim.NodeName.GetText()) == 0)
+ const SkeletonMapping skeletonMapping = GetSkeletonMapping(skeleton);
+ mappingData.SourceSkeleton = skeleton;
+ if (skeletonMapping.NodesMapping.Length() == nodesCount)
{
- result[j] = i;
- break;
+ const auto& nodes = skeleton->Skeleton.Nodes;
+ for (int32 j = 0; j < nodesCount; j++)
+ {
+ if (skeletonMapping.NodesMapping[j] != -1)
+ {
+ const Char* nodeName = nodes[skeletonMapping.NodesMapping[j]].Name.GetText();
+ for (int32 i = 0; i < channels.Count(); i++)
+ {
+ if (StringUtils::CompareIgnoreCase(nodeName, channels[i].NodeName.GetText()) == 0)
+ {
+ mappingData.NodesMapping[j] = i;
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+ else
+ {
+ #if !BUILD_RELEASE
+ LOG(Error, "Missing asset {0} to use for skeleton mapping of {1}", retarget->SkeletonAsset, ToString());
+ #endif
+ return mapping;
+ }
+ }
+ else
+ {
+ // Map animation channels to the skeleton nodes (by name)
+ for (int32 i = 0; i < channels.Count(); i++)
+ {
+ const NodeAnimationData& nodeAnim = channels[i];
+ for (int32 j = 0; j < nodesCount; j++)
+ {
+ if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), nodeAnim.NodeName.GetText()) == 0)
+ {
+ mappingData.NodesMapping[j] = i;
+ break;
+ }
}
}
}
}
else if (const auto* sourceModel = Cast(source))
{
- // Map source skeleton nodes to the target skeleton nodes (by name)
- const auto& nodes = sourceModel->Skeleton.Nodes;
- for (int32 i = 0; i < nodes.Count(); i++)
+ if (retarget)
{
- const SkeletonNode& node = nodes[i];
- for (int32 j = 0; j < nodesCount; j++)
+ // Use nodes retargeting
+ for (const auto& e : retarget->NodesMapping)
{
- if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), node.Name.GetText()) == 0)
+ const int32 dstIndex = Skeleton.FindNode(e.Key);
+ const int32 srcIndex = sourceModel->Skeleton.FindNode(e.Value);
+ if (dstIndex != -1 && srcIndex != -1)
{
- result[j] = i;
- break;
+ mappingData.NodesMapping[dstIndex] = srcIndex;
+ }
+ }
+ }
+ else
+ {
+ // Map source skeleton nodes to the target skeleton nodes (by name)
+ const auto& nodes = sourceModel->Skeleton.Nodes;
+ for (int32 i = 0; i < nodes.Count(); i++)
+ {
+ const SkeletonNode& node = nodes[i];
+ for (int32 j = 0; j < nodesCount; j++)
+ {
+ if (StringUtils::CompareIgnoreCase(Skeleton.Nodes[j].Name.GetText(), node.Name.GetText()) == 0)
+ {
+ mappingData.NodesMapping[j] = i;
+ break;
+ }
}
}
}
@@ -207,18 +274,20 @@ Span SkinnedModel::GetSkeletonMapping(Asset* source)
else
{
#if !BUILD_RELEASE
- LOG(Error, "Invalid asset type {0} to use for skeleton mapping", source->GetTypeName());
+ LOG(Error, "Invalid asset type {0} to use for skeleton mapping of {1}", source->GetTypeName(), ToString());
#endif
}
// Add to cache
- _skeletonMappingCache.Add(source, result);
+ _skeletonMappingCache.Add(source, mappingData);
source->OnUnloaded.Bind(this);
#if USE_EDITOR
source->OnReloading.Bind(this);
#endif
}
- return result;
+ mapping.SourceSkeleton = mappingData.SourceSkeleton;
+ mapping.NodesMapping = mappingData.NodesMapping;
+ return mapping;
}
bool SkinnedModel::Intersects(const Ray& ray, const Matrix& world, Real& distance, Vector3& normal, SkinnedMesh** mesh, int32 lodIndex)
@@ -374,10 +443,8 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes)
ScopeLock lock(model->Locker);
- // Setup nodes
+ // Setup
model->Skeleton.Nodes = nodes;
-
- // Setup bones
model->Skeleton.Bones.Resize(nodes.Count());
for (int32 i = 0; i < nodes.Count(); i++)
{
@@ -386,6 +453,7 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes)
model->Skeleton.Bones[i].LocalTransform = node.LocalTransform;
model->Skeleton.Bones[i].NodeIndex = i;
}
+ ClearSkeletonMapping();
// Calculate offset matrix (inverse bind pose transform) for every bone manually
for (int32 i = 0; i < model->Skeleton.Bones.Count(); i++)
@@ -420,11 +488,10 @@ bool SkinnedModel::SetupSkeleton(const Array& nodes, const ArrayLocker);
- // Setup nodes
+ // Setup
model->Skeleton.Nodes = nodes;
-
- // Setup bones
model->Skeleton.Bones = bones;
+ ClearSkeletonMapping();
// Calculate offset matrix (inverse bind pose transform) for every bone manually
if (autoCalculateOffsetMatrix)
@@ -481,6 +548,9 @@ bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
MemoryWriteStream headerStream(1024);
MemoryWriteStream* stream = &headerStream;
{
+ // Header Version
+ stream->WriteByte(1);
+
// Min Screen Size
stream->WriteFloat(MinScreenSize);
@@ -568,6 +638,17 @@ bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
stream->Write(bone.OffsetMatrix);
}
}
+
+ // Retargeting
+ {
+ stream->WriteInt32(_skeletonRetargets.Count());
+ for (const auto& retarget : _skeletonRetargets)
+ {
+ stream->Write(retarget.SourceAsset);
+ stream->Write(retarget.SkeletonAsset);
+ stream->Write(retarget.NodesMapping);
+ }
+ }
}
// Use a temporary chunks for data storage for virtual assets
@@ -621,8 +702,6 @@ bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
task->Start();
tasks.Add(task);
}
-
- // Wait for all
if (Task::WaitAll(tasks))
return true;
tasks.Clear();
@@ -631,12 +710,10 @@ bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
{
int32 dataSize = meshesCount * (2 * sizeof(uint32) + sizeof(bool));
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
- {
dataSize += meshesData[meshIndex].DataSize();
- }
-
- MemoryWriteStream meshesStream(dataSize);
+ MemoryWriteStream meshesStream(Math::RoundUpToPowerOf2(dataSize));
+ meshesStream.WriteByte(1);
for (int32 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
{
const auto& mesh = lod.Meshes[meshIndex];
@@ -666,10 +743,10 @@ bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
return true;
}
+ // #MODEL_DATA_FORMAT_USAGE
meshesStream.WriteUint32(vertices);
meshesStream.WriteUint32(triangles);
meshesStream.WriteUint16(mesh.BlendShapes.Count());
-
for (const auto& blendShape : mesh.BlendShapes)
{
meshesStream.WriteBool(blendShape.UseNormals);
@@ -678,9 +755,7 @@ bool SkinnedModel::Save(bool withMeshDataFromGpu, const StringView& path)
meshesStream.WriteUint32(blendShape.Vertices.Count());
meshesStream.WriteBytes(blendShape.Vertices.Get(), blendShape.Vertices.Count() * sizeof(BlendShapeVertex));
}
-
meshesStream.WriteBytes(meshData.VB0.Get(), vb0Size);
-
if (shouldUse16BitIndexBuffer == use16BitIndexBuffer)
{
meshesStream.WriteBytes(meshData.IB.Get(), ibSize);
@@ -759,8 +834,6 @@ bool SkinnedModel::Init(const Span& meshesCountPerLod)
// Setup
MaterialSlots.Resize(1);
MinScreenSize = 0.0f;
-
- // Setup LODs
for (int32 lodIndex = 0; lodIndex < LODs.Count(); lodIndex++)
LODs[lodIndex].Dispose();
LODs.Resize(meshesCountPerLod.Length());
@@ -789,6 +862,19 @@ bool SkinnedModel::Init(const Span& meshesCountPerLod)
return false;
}
+void SkinnedModel::ClearSkeletonMapping()
+{
+ for (auto& e : _skeletonMappingCache)
+ {
+ e.Key->OnUnloaded.Unbind(this);
+#if USE_EDITOR
+ e.Key->OnReloading.Unbind(this);
+#endif
+ Allocator::Free(e.Value.NodesMapping.Get());
+ }
+ _skeletonMappingCache.Clear();
+}
+
void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj)
{
ScopeLock lock(Locker);
@@ -802,7 +888,7 @@ void SkinnedModel::OnSkeletonMappingSourceAssetUnloaded(Asset* obj)
#endif
// Clear cache
- Allocator::Free(i->Value.Get());
+ Allocator::Free(i->Value.NodesMapping.Get());
_skeletonMappingCache.Remove(i);
}
@@ -814,7 +900,7 @@ uint64 SkinnedModel::GetMemoryUsage() const
result += Skeleton.GetMemoryUsage();
result += _skeletonMappingCache.Capacity() * sizeof(Dictionary>::Bucket);
for (const auto& e : _skeletonMappingCache)
- result += e.Value.Length();
+ result += e.Value.NodesMapping.Length();
Locker.Unlock();
return result;
}
@@ -853,6 +939,7 @@ void SkinnedModel::InitAsVirtual()
// Init with one mesh and single bone
int32 meshesCount = 1;
Init(ToSpan(&meshesCount, 1));
+ ClearSkeletonMapping();
Skeleton.Dispose();
//
Skeleton.Nodes.Resize(1);
@@ -973,6 +1060,9 @@ Asset::LoadResult SkinnedModel::load()
MemoryReadStream headerStream(chunk0->Get(), chunk0->Size());
ReadStream* stream = &headerStream;
+ // Header Version
+ byte version = stream->ReadByte();
+
// Min Screen Size
stream->ReadFloat(&MinScreenSize);
@@ -1071,12 +1161,9 @@ Asset::LoadResult SkinnedModel::load()
if (nodesCount <= 0)
return LoadResult::InvalidData;
Skeleton.Nodes.Resize(nodesCount, false);
-
- // For each node
for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++)
{
- auto& node = Skeleton.Nodes[nodeIndex];
-
+ auto& node = Skeleton.Nodes.Get()[nodeIndex];
stream->Read(node.ParentIndex);
stream->ReadTransform(&node.LocalTransform);
stream->ReadString(&node.Name, 71);
@@ -1087,12 +1174,9 @@ Asset::LoadResult SkinnedModel::load()
if (bonesCount <= 0)
return LoadResult::InvalidData;
Skeleton.Bones.Resize(bonesCount, false);
-
- // For each bone
for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
{
- auto& bone = Skeleton.Bones[boneIndex];
-
+ auto& bone = Skeleton.Bones.Get()[boneIndex];
stream->Read(bone.ParentIndex);
stream->Read(bone.NodeIndex);
stream->ReadTransform(&bone.LocalTransform);
@@ -1100,6 +1184,20 @@ Asset::LoadResult SkinnedModel::load()
}
}
+ // Retargeting
+ {
+ int32 entriesCount;
+ stream->ReadInt32(&entriesCount);
+ _skeletonRetargets.Resize(entriesCount);
+ for (int32 entryIndex = 0; entryIndex < entriesCount; entryIndex++)
+ {
+ auto& retarget = _skeletonRetargets[entryIndex];
+ stream->Read(retarget.SourceAsset);
+ stream->Read(retarget.SkeletonAsset);
+ stream->Read(retarget.NodesMapping);
+ }
+ }
+
// Request resource streaming
StartStreaming(true);
@@ -1123,15 +1221,8 @@ void SkinnedModel::unload(bool isReloading)
LODs.Clear();
Skeleton.Dispose();
_loadedLODs = 0;
- for (auto& e : _skeletonMappingCache)
- {
- e.Key->OnUnloaded.Unbind(this);
-#if USE_EDITOR
- e.Key->OnReloading.Unbind(this);
-#endif
- Allocator::Free(e.Value.Get());
- }
- _skeletonMappingCache.Clear();
+ _skeletonRetargets.Clear();
+ ClearSkeletonMapping();
}
bool SkinnedModel::init(AssetInitData& initData)
diff --git a/Source/Engine/Content/Assets/SkinnedModel.h b/Source/Engine/Content/Assets/SkinnedModel.h
index ea250244e..6e40e9665 100644
--- a/Source/Engine/Content/Assets/SkinnedModel.h
+++ b/Source/Engine/Content/Assets/SkinnedModel.h
@@ -14,13 +14,31 @@ class StreamSkinnedModelLODTask;
///
API_CLASS(NoSpawn) class FLAXENGINE_API SkinnedModel : public ModelBase
{
- DECLARE_BINARY_ASSET_HEADER(SkinnedModel, 4);
+ DECLARE_BINARY_ASSET_HEADER(SkinnedModel, 5);
friend SkinnedMesh;
friend StreamSkinnedModelLODTask;
+public:
+ // Skeleton mapping descriptor.
+ struct FLAXENGINE_API SkeletonMapping
+ {
+ // Target skeleton.
+ AssetReference TargetSkeleton;
+ // Source skeleton.
+ AssetReference SourceSkeleton;
+ // The node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each item is index of the source skeleton node into target skeleton node.
+ Span NodesMapping;
+ };
+
private:
+ struct SkeletonMappingData
+ {
+ AssetReference SourceSkeleton;
+ Span NodesMapping;
+ };
+
int32 _loadedLODs = 0;
StreamSkinnedModelLODTask* _streamingTask = nullptr;
- Dictionary> _skeletonMappingCache;
+ Dictionary _skeletonMappingCache;
public:
///
@@ -108,7 +126,7 @@ public:
///
/// The name of the node.
/// The index of the node or -1 if not found.
- API_FUNCTION() FORCE_INLINE int32 FindNode(const StringView& name)
+ API_FUNCTION() FORCE_INLINE int32 FindNode(const StringView& name) const
{
return Skeleton.FindNode(name);
}
@@ -118,7 +136,7 @@ public:
///
/// The name of the node used by the bone.
/// The index of the bone or -1 if not found.
- API_FUNCTION() FORCE_INLINE int32 FindBone(const StringView& name)
+ API_FUNCTION() FORCE_INLINE int32 FindBone(const StringView& name) const
{
return FindBone(FindNode(name));
}
@@ -128,7 +146,7 @@ public:
///
/// The index of the node.
/// The index of the bone or -1 if not found.
- API_FUNCTION() FORCE_INLINE int32 FindBone(int32 nodeIndex)
+ API_FUNCTION() FORCE_INLINE int32 FindBone(int32 nodeIndex) const
{
return Skeleton.FindBone(nodeIndex);
}
@@ -157,8 +175,8 @@ public:
/// Gets the skeleton mapping for a given asset (animation or other skinned model). Uses identity mapping or manually created retargeting setup.
///
/// The source asset (animation or other skinned model) to get mapping to its skeleton.
- /// The cached node-to-node mapping for the fast animation sampling for the skinned model skeleton nodes. Each span item is index of the source skeleton node for this skeleton.
- Span GetSkeletonMapping(Asset* source);
+ /// The skeleton mapping for the source asset into this skeleton.
+ SkeletonMapping GetSkeletonMapping(Asset* source);
///
/// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance.
@@ -265,8 +283,35 @@ private:
/// True if failed, otherwise false.
bool Init(const Span& meshesCountPerLod);
+ void ClearSkeletonMapping();
void OnSkeletonMappingSourceAssetUnloaded(Asset* obj);
+#if USE_EDITOR
+public:
+ // Skeleton retargeting setup (internal use only - accessed by Editor)
+ API_STRUCT(NoDefault) struct SkeletonRetarget
+ {
+ DECLARE_SCRIPTING_TYPE_MINIMAL(SkeletonRetarget);
+ // Source asset id.
+ API_FIELD() Guid SourceAsset;
+ // Skeleton asset id to use for remapping.
+ API_FIELD() Guid SkeletonAsset;
+ // Skeleton nodes remapping table (maps this skeleton node name to other skeleton node).
+ API_FIELD() Dictionary NodesMapping;
+ };
+ // Gets or sets the skeleton retarget entries (accessed in Editor only).
+ API_PROPERTY() const Array& GetSkeletonRetargets() const { return _skeletonRetargets; }
+ API_PROPERTY() void SetSkeletonRetargets(const Array& value) { Locker.Lock(); _skeletonRetargets = value; ClearSkeletonMapping(); Locker.Unlock(); }
+private:
+#else
+ struct SkeletonRetarget
+ {
+ Guid SourceAsset, SkeletonAsset;
+ Dictionary NodesMapping;
+ };
+#endif
+ Array _skeletonRetargets;
+
public:
// [ModelBase]
uint64 GetMemoryUsage() const override;
diff --git a/Source/Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h b/Source/Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h
index cd3eb75d1..49be3b347 100644
--- a/Source/Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h
+++ b/Source/Engine/Content/Upgraders/SkinnedModelAssetUpgrader.h
@@ -28,9 +28,10 @@ public:
{
static const Upgrader upgraders[] =
{
- { 1, 2, &Upgrade_1_To_2 },
- { 2, 3, &Upgrade_2_To_3 },
- { 3, 4, &Upgrade_3_To_4 },
+ { 1, 2, &Upgrade_1_To_2 }, // [Deprecated on 28.04.2023, expires on 01.01.2024]
+ { 2, 3, &Upgrade_2_To_3 }, // [Deprecated on 28.04.2023, expires on 01.01.2024]
+ { 3, 4, &Upgrade_3_To_4 }, // [Deprecated on 28.04.2023, expires on 01.01.2024]
+ { 4, 5, &Upgrade_4_To_5 }, // [Deprecated on 28.04.2023, expires on 28.04.2026]
};
setup(upgraders, ARRAY_COUNT(upgraders));
}
@@ -57,7 +58,6 @@ private:
MemoryWriteStream output(srcData->Size());
do
{
- // #MODEL_DATA_FORMAT_USAGE
uint32 vertices;
stream.ReadUint32(&vertices);
uint32 triangles;
@@ -422,6 +422,241 @@ private:
return false;
}
+
+ static bool Upgrade_4_To_5(AssetMigrationContext& context)
+ {
+ ASSERT(context.Input.SerializedVersion == 4 && context.Output.SerializedVersion == 5);
+
+ // Changes:
+ // - added version number to header (for easier changes in future)
+ // - added version number to mesh data (for easier changes in future)
+ // - added skeleton retarget setups to header
+
+ // Rewrite header chunk (added header version and retarget entries)
+ byte lodCount;
+ Array meshesCounts;
+ {
+ const auto srcData = context.Input.Header.Chunks[0];
+ if (srcData == nullptr || srcData->IsMissing())
+ {
+ LOG(Warning, "Missing model header chunk");
+ return true;
+ }
+ MemoryReadStream stream(srcData->Get(), srcData->Size());
+ MemoryWriteStream output(srcData->Size());
+
+ // Header Version
+ output.WriteByte(1);
+
+ // Min Screen Size
+ float minScreenSize;
+ stream.ReadFloat(&minScreenSize);
+ output.WriteFloat(minScreenSize);
+
+ // Amount of material slots
+ int32 materialSlotsCount;
+ stream.ReadInt32(&materialSlotsCount);
+ output.WriteInt32(materialSlotsCount);
+
+ // For each material slot
+ for (int32 materialSlotIndex = 0; materialSlotIndex < materialSlotsCount; materialSlotIndex++)
+ {
+ // Material
+ Guid materialId;
+ stream.Read(materialId);
+ output.Write(materialId);
+
+ // Shadows Mode
+ output.WriteByte(stream.ReadByte());
+
+ // Name
+ String name;
+ stream.ReadString(&name, 11);
+ output.WriteString(name, 11);
+ }
+
+ // Amount of LODs
+ stream.ReadByte(&lodCount);
+ output.WriteByte(lodCount);
+ meshesCounts.Resize(lodCount);
+
+ // For each LOD
+ for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
+ {
+ // Screen Size
+ float screenSize;
+ stream.ReadFloat(&screenSize);
+ output.WriteFloat(screenSize);
+
+ // Amount of meshes
+ uint16 meshesCount;
+ stream.ReadUint16(&meshesCount);
+ output.WriteUint16(meshesCount);
+ meshesCounts[lodIndex] = meshesCount;
+
+ // For each mesh
+ for (uint16 meshIndex = 0; meshIndex < meshesCount; meshIndex++)
+ {
+ // Material Slot index
+ int32 materialSlotIndex;
+ stream.ReadInt32(&materialSlotIndex);
+ output.WriteInt32(materialSlotIndex);
+
+ // Box
+ BoundingBox box;
+ stream.Read(box);
+ output.Write(box);
+
+ // Sphere
+ BoundingSphere sphere;
+ stream.Read(sphere);
+ output.Write(sphere);
+
+ // Blend Shapes
+ uint16 blendShapes;
+ stream.ReadUint16(&blendShapes);
+ output.WriteUint16(blendShapes);
+ for (int32 blendShapeIndex = 0; blendShapeIndex < blendShapes; blendShapeIndex++)
+ {
+ String blendShapeName;
+ stream.ReadString(&blendShapeName, 13);
+ output.WriteString(blendShapeName, 13);
+ float blendShapeWeight;
+ stream.ReadFloat(&blendShapeWeight);
+ output.WriteFloat(blendShapeWeight);
+ }
+ }
+ }
+
+ // Skeleton
+ {
+ int32 nodesCount;
+ stream.ReadInt32(&nodesCount);
+ output.WriteInt32(nodesCount);
+
+ // For each node
+ for (int32 nodeIndex = 0; nodeIndex < nodesCount; nodeIndex++)
+ {
+ int32 parentIndex;
+ stream.ReadInt32(&parentIndex);
+ output.WriteInt32(parentIndex);
+
+ Transform localTransform;
+ stream.Read(localTransform);
+ output.Write(localTransform);
+
+ String name;
+ stream.ReadString(&name, 71);
+ output.WriteString(name, 71);
+ }
+
+ int32 bonesCount;
+ stream.ReadInt32(&bonesCount);
+ output.WriteInt32(bonesCount);
+
+ // For each bone
+ for (int32 boneIndex = 0; boneIndex < bonesCount; boneIndex++)
+ {
+ int32 parentIndex;
+ stream.ReadInt32(&parentIndex);
+ output.WriteInt32(parentIndex);
+
+ int32 nodeIndex;
+ stream.ReadInt32(&nodeIndex);
+ output.WriteInt32(nodeIndex);
+
+ Transform localTransform;
+ stream.Read(localTransform);
+ output.Write(localTransform);
+
+ Matrix offsetMatrix;
+ stream.ReadBytes(&offsetMatrix, sizeof(Matrix));
+ output.WriteBytes(&offsetMatrix, sizeof(Matrix));
+ }
+ }
+
+ // Retargeting
+ {
+ output.WriteInt32(0);
+ }
+
+ // Save new data
+ if (stream.GetPosition() != stream.GetLength())
+ {
+ LOG(Error, "Invalid position after upgrading skinned model header data.");
+ return true;
+ }
+ if (context.AllocateChunk(0))
+ return true;
+ context.Output.Header.Chunks[0]->Data.Copy(output.GetHandle(), output.GetPosition());
+ }
+
+ // Rewrite meshes data chunks
+ for (int32 lodIndex = 0; lodIndex < lodCount; lodIndex++)
+ {
+ const int32 chunkIndex = lodIndex + 1;
+ const auto srcData = context.Input.Header.Chunks[chunkIndex];
+ if (srcData == nullptr || srcData->IsMissing())
+ {
+ LOG(Warning, "Missing skinned model LOD meshes data chunk");
+ return true;
+ }
+ MemoryReadStream stream(srcData->Get(), srcData->Size());
+ MemoryWriteStream output(srcData->Size());
+
+ // Mesh Data Version
+ output.WriteByte(1);
+
+ for (int32 meshIndex = 0; meshIndex < meshesCounts[lodIndex]; meshIndex++)
+ {
+ uint32 vertices;
+ stream.ReadUint32(&vertices);
+ output.WriteUint32(vertices);
+ uint32 triangles;
+ stream.ReadUint32(&triangles);
+ output.WriteUint32(triangles);
+ uint16 blendShapesCount;
+ stream.ReadUint16(&blendShapesCount);
+ output.WriteUint16(blendShapesCount);
+ for (int32 blendShapeIndex = 0; blendShapeIndex < blendShapesCount; blendShapeIndex++)
+ {
+ output.WriteBool(stream.ReadBool());
+ uint32 minVertexIndex, maxVertexIndex;
+ stream.ReadUint32(&minVertexIndex);
+ output.WriteUint32(minVertexIndex);
+ stream.ReadUint32(&maxVertexIndex);
+ output.WriteUint32(maxVertexIndex);
+ uint32 blendShapeVertices;
+ stream.ReadUint32(&blendShapeVertices);
+ output.WriteUint32(blendShapeVertices);
+ const uint32 blendShapeDataSize = blendShapeVertices * sizeof(BlendShapeVertex);
+ const auto blendShapeData = stream.Move(blendShapeDataSize);
+ output.WriteBytes(blendShapeData, blendShapeDataSize);
+ }
+ const uint32 indicesCount = triangles * 3;
+ const bool use16BitIndexBuffer = indicesCount <= MAX_uint16;
+ const uint32 ibStride = use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32);
+ if (vertices == 0 || triangles == 0)
+ return true;
+ const auto vb0 = stream.Move(vertices);
+ output.WriteBytes(vb0, vertices * sizeof(VB0SkinnedElementType));
+ const auto ib = stream.Move(indicesCount * ibStride);
+ output.WriteBytes(ib, indicesCount * ibStride);
+ }
+
+ // Save new data
+ if (stream.GetPosition() != stream.GetLength())
+ {
+ LOG(Error, "Invalid position after upgrading skinned model LOD meshes data.");
+ return true;
+ }
+ if (context.AllocateChunk(chunkIndex))
+ return true;
+ context.Output.Header.Chunks[chunkIndex]->Data.Copy(output.GetHandle(), output.GetPosition());
+ }
+
+ return false;
+ }
};
#endif
diff --git a/Source/Engine/ContentExporters/ExportModel.cpp b/Source/Engine/ContentExporters/ExportModel.cpp
index 1134108e6..89f37c8e5 100644
--- a/Source/Engine/ContentExporters/ExportModel.cpp
+++ b/Source/Engine/ContentExporters/ExportModel.cpp
@@ -160,6 +160,18 @@ ExportAssetResult AssetExporters::ExportSkinnedModel(ExportAssetContext& context
stream.ReadUint32(&vertices);
uint32 triangles;
stream.ReadUint32(&triangles);
+ uint16 blendShapesCount;
+ stream.ReadUint16(&blendShapesCount);
+ for (int32 blendShapeIndex = 0; blendShapeIndex < blendShapesCount; blendShapeIndex++)
+ {
+ stream.ReadBool();
+ uint32 tmp;
+ stream.ReadUint32(&tmp);
+ stream.ReadUint32(&tmp);
+ uint32 blendShapeVertices;
+ stream.ReadUint32(&blendShapeVertices);
+ stream.Move(blendShapeVertices * sizeof(BlendShapeVertex));
+ }
uint32 indicesCount = triangles * 3;
bool use16BitIndexBuffer = indicesCount <= MAX_uint16;
uint32 ibStride = use16BitIndexBuffer ? sizeof(uint16) : sizeof(uint32);
diff --git a/Source/Engine/ContentImporters/ImportModelFile.cpp b/Source/Engine/ContentImporters/ImportModelFile.cpp
index 8851ee4ce..7e50770f1 100644
--- a/Source/Engine/ContentImporters/ImportModelFile.cpp
+++ b/Source/Engine/ContentImporters/ImportModelFile.cpp
@@ -269,6 +269,9 @@ CreateAssetResult ImportModelFile::ImportSkinnedModel(CreateAssetContext& contex
{
stream.SetPosition(0);
+ // Mesh Data Version
+ stream.WriteByte(1);
+
// Pack meshes
auto& meshes = modelData.LODs[lodIndex].Meshes;
for (int32 meshIndex = 0; meshIndex < meshes.Count(); meshIndex++)
diff --git a/Source/Engine/Graphics/Models/ModelData.cpp b/Source/Engine/Graphics/Models/ModelData.cpp
index 3522e350b..591d328b1 100644
--- a/Source/Engine/Graphics/Models/ModelData.cpp
+++ b/Source/Engine/Graphics/Models/ModelData.cpp
@@ -782,6 +782,9 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
return true;
}
+ // Version
+ stream->WriteByte(1);
+
// Min Screen Size
stream->WriteFloat(MinScreenSize);
@@ -792,7 +795,6 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
for (int32 materialSlotIndex = 0; materialSlotIndex < Materials.Count(); materialSlotIndex++)
{
auto& slot = Materials[materialSlotIndex];
-
stream->Write(slot.AssetID);
stream->WriteByte(static_cast(slot.ShadowsMode));
stream->WriteString(slot.Name, 11);
@@ -878,6 +880,11 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
}
}
+ // Retargeting
+ {
+ stream->WriteInt32(0);
+ }
+
return false;
}
diff --git a/Source/Engine/Graphics/Models/SkeletonData.h b/Source/Engine/Graphics/Models/SkeletonData.h
index 1c9d212a9..816f69c85 100644
--- a/Source/Engine/Graphics/Models/SkeletonData.h
+++ b/Source/Engine/Graphics/Models/SkeletonData.h
@@ -112,7 +112,7 @@ public:
Bones.Swap(other.Bones);
}
- int32 FindNode(const StringView& name)
+ int32 FindNode(const StringView& name) const
{
for (int32 i = 0; i < Nodes.Count(); i++)
{
@@ -122,7 +122,7 @@ public:
return -1;
}
- int32 FindBone(int32 nodeIndex)
+ int32 FindBone(int32 nodeIndex) const
{
for (int32 i = 0; i < Bones.Count(); i++)
{
diff --git a/Source/Engine/Graphics/Models/SkinnedMesh.cpp b/Source/Engine/Graphics/Models/SkinnedMesh.cpp
index 2b8cf5c3a..b6c966508 100644
--- a/Source/Engine/Graphics/Models/SkinnedMesh.cpp
+++ b/Source/Engine/Graphics/Models/SkinnedMesh.cpp
@@ -331,6 +331,7 @@ bool SkinnedMesh::DownloadDataCPU(MeshBufferType type, BytesContainer& result, i
MemoryReadStream stream(chunk->Get(), chunk->Size());
// Seek to find mesh location
+ byte version = stream.ReadByte();
for (int32 i = 0; i <= _index; i++)
{
// #MODEL_DATA_FORMAT_USAGE
diff --git a/Source/Engine/Graphics/Models/SkinnedModelLOD.cpp b/Source/Engine/Graphics/Models/SkinnedModelLOD.cpp
index d9e34c714..eb461fa58 100644
--- a/Source/Engine/Graphics/Models/SkinnedModelLOD.cpp
+++ b/Source/Engine/Graphics/Models/SkinnedModelLOD.cpp
@@ -15,6 +15,7 @@ bool SkinnedModelLOD::HasAnyMeshInitialized() const
bool SkinnedModelLOD::Load(MemoryReadStream& stream)
{
// Load LOD for each mesh
+ byte version = stream.ReadByte();
for (int32 i = 0; i < Meshes.Count(); i++)
{
auto& mesh = Meshes[i];