Add **skeleton retargeting** to play animations on different skeletons

This commit is contained in:
Wojtek Figat
2023-05-04 11:54:17 +02:00
parent b89d32ce2b
commit 05ffaf7cef
15 changed files with 775 additions and 104 deletions

View File

@@ -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>

View File

@@ -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;
}
}
}

View File

@@ -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())

View File

@@ -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())
{

View File

@@ -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;
}
}
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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;

View File

@@ -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

View File

@@ -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);

View File

@@ -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++)

View File

@@ -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;
}

View File

@@ -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++)
{

View File

@@ -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

View File

@@ -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];