Add **skeleton retargeting** to play animations on different skeletons
This commit is contained in:
@@ -321,6 +321,9 @@
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=Reimports/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=reimported/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=renderable/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=retarget/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=retargeting/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=retargets/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=reverb/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=rigidbodies/@EntryIndexedValue">True</s:Boolean>
|
||||
<s:Boolean x:Key="/Default/UserDictionary/Words/=rigidbody/@EntryIndexedValue">True</s:Boolean>
|
||||
|
||||
@@ -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
|
||||
/// <inheritdoc />
|
||||
public override void OnLayoutDeserialize()
|
||||
{
|
||||
_split.SplitterValue = 0.65f;
|
||||
_split.SplitterValue = 0.59f;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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<string, string> NodesMapping;
|
||||
}
|
||||
|
||||
internal Dictionary<Asset, SetupProxy> 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<Asset, SetupProxy>();
|
||||
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<SkinnedModel>(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<AssetPicker>().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<AssetPicker>().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<string, string>();
|
||||
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<ComboBox>().CustomControl;
|
||||
comboBox.AddItems(items);
|
||||
comboBox.Tag = new KeyValuePair<string, Asset>(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<AssetPicker>().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<string, Asset>)comboBox.Tag).Value;
|
||||
var nodeMappingKey = ((KeyValuePair<string, Asset>)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())
|
||||
{
|
||||
|
||||
@@ -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<AnimGraphContext> 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<int32> 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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int32> 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)
|
||||
|
||||
@@ -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<int32> SkinnedModel::GetSkeletonMapping(Asset* source)
|
||||
SkinnedModel::SkeletonMapping SkinnedModel::GetSkeletonMapping(Asset* source)
|
||||
{
|
||||
SkeletonMapping mapping;
|
||||
mapping.TargetSkeleton = this;
|
||||
if (WaitForLoaded() || !source || source->WaitForLoaded())
|
||||
return Span<int32>();
|
||||
return mapping;
|
||||
ScopeLock lock(Locker);
|
||||
Span<int32> 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>((int32*)Allocator::Allocate(nodesCount * sizeof(int32)), nodesCount);
|
||||
mappingData.NodesMapping = Span<int32>((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<Animation>(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<SkinnedModel>(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<SkinnedModel>(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<int32> 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<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
#if USE_EDITOR
|
||||
source->OnReloading.Bind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(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<SkeletonNode>& 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<SkeletonNode>& 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<SkeletonNode>& nodes, const Array<S
|
||||
|
||||
ScopeLock lock(model->Locker);
|
||||
|
||||
// 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<int32>& 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<int32>& meshesCountPerLod)
|
||||
return false;
|
||||
}
|
||||
|
||||
void SkinnedModel::ClearSkeletonMapping()
|
||||
{
|
||||
for (auto& e : _skeletonMappingCache)
|
||||
{
|
||||
e.Key->OnUnloaded.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
#if USE_EDITOR
|
||||
e.Key->OnReloading.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(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<Asset*, Span<int32>>::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<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
#if USE_EDITOR
|
||||
e.Key->OnReloading.Unbind<SkinnedModel, &SkinnedModel::OnSkeletonMappingSourceAssetUnloaded>(this);
|
||||
#endif
|
||||
Allocator::Free(e.Value.Get());
|
||||
}
|
||||
_skeletonMappingCache.Clear();
|
||||
_skeletonRetargets.Clear();
|
||||
ClearSkeletonMapping();
|
||||
}
|
||||
|
||||
bool SkinnedModel::init(AssetInitData& initData)
|
||||
|
||||
@@ -14,13 +14,31 @@ class StreamSkinnedModelLODTask;
|
||||
/// </summary>
|
||||
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<SkinnedModel> TargetSkeleton;
|
||||
// Source skeleton.
|
||||
AssetReference<SkinnedModel> 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<int32> NodesMapping;
|
||||
};
|
||||
|
||||
private:
|
||||
struct SkeletonMappingData
|
||||
{
|
||||
AssetReference<SkinnedModel> SourceSkeleton;
|
||||
Span<int32> NodesMapping;
|
||||
};
|
||||
|
||||
int32 _loadedLODs = 0;
|
||||
StreamSkinnedModelLODTask* _streamingTask = nullptr;
|
||||
Dictionary<Asset*, Span<int32>> _skeletonMappingCache;
|
||||
Dictionary<Asset*, SkeletonMappingData> _skeletonMappingCache;
|
||||
|
||||
public:
|
||||
/// <summary>
|
||||
@@ -108,7 +126,7 @@ public:
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the node.</param>
|
||||
/// <returns>The index of the node or -1 if not found.</returns>
|
||||
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:
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the node used by the bone.</param>
|
||||
/// <returns>The index of the bone or -1 if not found.</returns>
|
||||
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:
|
||||
/// </summary>
|
||||
/// <param name="nodeIndex">The index of the node.</param>
|
||||
/// <returns>The index of the bone or -1 if not found.</returns>
|
||||
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.
|
||||
/// </summary>
|
||||
/// <param name="source">The source asset (animation or other skinned model) to get mapping to its skeleton.</param>
|
||||
/// <returns>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.</returns>
|
||||
Span<int32> GetSkeletonMapping(Asset* source);
|
||||
/// <returns>The skeleton mapping for the source asset into this skeleton.</returns>
|
||||
SkeletonMapping GetSkeletonMapping(Asset* source);
|
||||
|
||||
/// <summary>
|
||||
/// Determines if there is an intersection between the SkinnedModel and a Ray in given world using given instance.
|
||||
@@ -265,8 +283,35 @@ private:
|
||||
/// <returns>True if failed, otherwise false.</returns>
|
||||
bool Init(const Span<int32>& 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<String, String, HeapAllocation> NodesMapping;
|
||||
};
|
||||
// Gets or sets the skeleton retarget entries (accessed in Editor only).
|
||||
API_PROPERTY() const Array<SkeletonRetarget>& GetSkeletonRetargets() const { return _skeletonRetargets; }
|
||||
API_PROPERTY() void SetSkeletonRetargets(const Array<SkeletonRetarget>& value) { Locker.Lock(); _skeletonRetargets = value; ClearSkeletonMapping(); Locker.Unlock(); }
|
||||
private:
|
||||
#else
|
||||
struct SkeletonRetarget
|
||||
{
|
||||
Guid SourceAsset, SkeletonAsset;
|
||||
Dictionary<String, String, HeapAllocation> NodesMapping;
|
||||
};
|
||||
#endif
|
||||
Array<SkeletonRetarget> _skeletonRetargets;
|
||||
|
||||
public:
|
||||
// [ModelBase]
|
||||
uint64 GetMemoryUsage() const override;
|
||||
|
||||
@@ -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<uint16> 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<byte>(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<VB0SkinnedElementType>(vertices);
|
||||
output.WriteBytes(vb0, vertices * sizeof(VB0SkinnedElementType));
|
||||
const auto ib = stream.Move<byte>(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
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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<byte>(slot.ShadowsMode));
|
||||
stream->WriteString(slot.Name, 11);
|
||||
@@ -878,6 +880,11 @@ bool ModelData::Pack2SkinnedModelHeader(WriteStream* stream) const
|
||||
}
|
||||
}
|
||||
|
||||
// Retargeting
|
||||
{
|
||||
stream->WriteInt32(0);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -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++)
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
|
||||
Reference in New Issue
Block a user