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