From fd39cac3e09dd193eb87e6299f5f935607d12dba Mon Sep 17 00:00:00 2001 From: ole kristian homelien Date: Tue, 21 Apr 2026 21:20:55 +0200 Subject: [PATCH 1/5] First pass --- AssetEditor/Themes/Controls.xaml.cs | 35 ++- Documentation/AssetEditorDocumentation.html | 1 + .../Documentation/Kitbash_PinTool.html | 204 ++++++++++++++++++ .../MeshFitter/MeshFitterWindow.xaml | 2 +- .../Commands/PinMeshToVertexCommand.cs | 2 +- .../Commands/SkinWrapRiggingCommand.cs | 30 +-- .../ChildEditors/PinTool/PinToolViewModel.cs | 3 +- .../PinTool/Presentation/PinToolWindow.xaml | 39 ++-- .../ChildEditors/PinTool/RegiggingHelper.cs | 145 ++++++++----- .../ChildEditors/PinTool/SkinWrapAlgorithm.cs | 53 +++-- 10 files changed, 392 insertions(+), 122 deletions(-) create mode 100644 Documentation/Documentation/Kitbash_PinTool.html diff --git a/AssetEditor/Themes/Controls.xaml.cs b/AssetEditor/Themes/Controls.xaml.cs index df10e3f51..d4607af68 100644 --- a/AssetEditor/Themes/Controls.xaml.cs +++ b/AssetEditor/Themes/Controls.xaml.cs @@ -35,16 +35,41 @@ private void Help_Event(object sender, RoutedEventArgs e) if (window == null || string.IsNullOrWhiteSpace(window.HelpDocumentPath)) return; - var helpPath = Path.IsPathRooted(window.HelpDocumentPath) - ? window.HelpDocumentPath - : Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, window.HelpDocumentPath)); + var rawPath = window.HelpDocumentPath; + var queryString = ""; + var queryIndex = rawPath.IndexOf('?'); + if (queryIndex >= 0) + { + queryString = rawPath.Substring(queryIndex); + rawPath = rawPath.Substring(0, queryIndex); + } + + var helpPath = Path.IsPathRooted(rawPath) + ? rawPath + : Path.GetFullPath(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, rawPath)); + + if (!File.Exists(helpPath) && Debugger.IsAttached) + { + var searchDir = new DirectoryInfo(AppDomain.CurrentDomain.BaseDirectory); + while (searchDir?.Parent != null) + { + searchDir = searchDir.Parent; + var candidate = Path.Combine(searchDir.FullName, rawPath); + if (File.Exists(candidate)) + { + helpPath = candidate; + break; + } + } + } - if (File.Exists(helpPath) == false) + if (!File.Exists(helpPath)) return; + var fileUri = new Uri(helpPath).AbsoluteUri + queryString; Process.Start(new ProcessStartInfo { - FileName = helpPath, + FileName = fileUri, UseShellExecute = true }); } diff --git a/Documentation/AssetEditorDocumentation.html b/Documentation/AssetEditorDocumentation.html index 7e78bca67..362b27b3e 100644 --- a/Documentation/AssetEditorDocumentation.html +++ b/Documentation/AssetEditorDocumentation.html @@ -110,6 +110,7 @@ { title: "Kitbashing", children: [ { title: "Basics", base: "kitbash_basics" }, { title: "Mesh Fitter", base: "Kitbash_MeshFitter" }, + { title: "Pin Tool", base: "Kitbash_PinTool" }, { title: "Photo Studio", base: "kitbash_photostudio" } ] } ]; diff --git a/Documentation/Documentation/Kitbash_PinTool.html b/Documentation/Documentation/Kitbash_PinTool.html new file mode 100644 index 000000000..3dfd5fe8b --- /dev/null +++ b/Documentation/Documentation/Kitbash_PinTool.html @@ -0,0 +1,204 @@ + + + + + Pin Tool + + + + + +

Pin Tool

+ +

+ The Pin Tool transfers bone weight and rigging information from one or more source meshes to a set of target meshes. + This is used when you have imported or created new geometry that needs to follow an animated skeleton, but does not yet have the correct bone indices or blend weights. +

+ + + Pin Tool UI overview + +

+ The tool has two modes that solve different problems: +

+ + + +
+ Short version: Use Pin for rigid attachments (weapons, shields, buckles). + Use Skin Wrap for deformable things (clothing, capes, skin, armor that should bend with the body). +
+ +

What problems it solves

+ + + +

Typical use cases

+ + + +

Before you open it

+ + + +

How to use — Pin mode

+ + + Pin mode workflow + +
    +
  1. Open the Pin Tool.
  2. +
  3. Set the mode to Pin.
  4. +
  5. In the viewport, select the mesh(es) that should receive rigging.
  6. +
  7. Press Add selected meshes to add them to the target list.
  8. +
  9. Switch to vertex selection mode in the viewport.
  10. +
  11. Select a vertex on the already-rigged source mesh at the point where you want the target to attach.
  12. +
  13. Press Set from selected Vertex to capture the source vertex.
  14. +
  15. Press Apply. Every vertex in the target meshes will receive the bone weights from that single source vertex.
  16. +
+ +
+ Tip: Pick a vertex that is on the bone you want the rigid object to follow. + If the source vertex has influence from multiple bones, the target will inherit that same mix — which is usually not what you want for a rigid attachment. + Choose a vertex with a single dominant bone for a clean result. +
+ +

How to use — Skin Wrap mode

+ + + Skin Wrap mode workflow + +
    +
  1. Open the Pin Tool.
  2. +
  3. Set the mode to Skin Wrap.
  4. +
  5. In the viewport, select the mesh(es) that should receive rigging.
  6. +
  7. Press Add selected meshes to add them to the target list.
  8. +
  9. In the viewport, select the rigged source mesh(es) you want to transfer weights from.
  10. +
  11. Press Add selected meshes in the source area to add them.
  12. +
  13. Press Apply. For each vertex in each target mesh, the tool finds the closest point on any source mesh surface and interpolates the bone weights from that triangle.
  14. +
+ + + Skin Wrap before and after + +
+ Multiple source meshes: You can add more than one source mesh. + The tool will find the closest point across all source meshes for each target vertex. + This is useful when different body parts (torso, arms, legs) are separate meshes. +
+ +

What each control does

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ControlWhat it does
Animation transfer modeSwitches between Pin and Skin Wrap.
Apply animation to — Add selected meshesAdds the currently selected meshes in the viewport to the target list.
Apply animation to — Remove allClears the target mesh list.
Set from selected Vertex (Pin mode)Captures the currently selected vertex and its mesh as the rigging source.
Add selected meshes (Skin Wrap source)Adds selected viewport meshes to the source list for weight transfer.
Remove all (Skin Wrap source)Clears the source mesh list.
ApplyExecutes the rigging transfer and closes the window.
+ +

What to watch out for

+ + + +
+ Important: Skin Wrap works best when the target mesh closely overlaps the source mesh in 3D space. + If the meshes are far apart or in very different poses, the nearest-point lookup will produce poor results. + Align the meshes before running the tool. +
+ +

Pin vs Skin Wrap — when to use which

+ + + + + + + + + + + + + + + + + + + + + + + + + + +
Use Pin when…Use Skin Wrap when…
The object should move rigidly (weapon, shield, ornament)The object should deform with the body (clothing, skin, flexible armor)
You want all vertices to follow the same bone(s)You want each vertex to get weights from its nearest surface point
You know exactly which bone the object should attach toYou have a rigged body mesh that covers the same area as the target
Quick — just select one vertexAutomatic — processes every vertex based on proximity
+ +

When it struggles

+ + + + + diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml b/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml index 4d87cf7ea..5d2bd16c6 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/MeshFitter/MeshFitterWindow.xaml @@ -10,7 +10,7 @@ xmlns:loc="clr-namespace:Shared.Ui.Common;assembly=Shared.Ui" mc:Ignorable="d" Style="{StaticResource CustomWindowStyle}" - HelpDocumentPath="Documentation\AssetEditorDocumentation.html" + HelpDocumentPath="Documentation\AssetEditorDocumentation.html?doc=Kitbash_MeshFitter" Title="{loc:Loc KitbashTool.MeshFitterTool.Title}" Height="500" Width="1000"> diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs index 2922a710e..62db42a2f 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/PinMeshToVertexCommand.cs @@ -45,9 +45,9 @@ public void Execute() currentMesh.Geometry.ChangeVertexType(_source.Geometry.VertexFormat, false); currentMesh.Geometry.UpdateSkeletonName(_source.Geometry.SkeletonName); + currentMesh.PivotPoint = Vector3.Zero; for (var i = 0; i < currentMesh.Geometry.VertexCount(); i++) { - currentMesh.PivotPoint = Vector3.Zero; currentMesh.Geometry.SetVertexBlendIndex(i, sourceVert.BlendIndices); currentMesh.Geometry.SetVertexWeights(i, sourceVert.BlendWeights); } diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs index 4ebea77f9..437dd3a09 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Commands/SkinWrapRiggingCommand.cs @@ -13,44 +13,44 @@ public class SkinWrapRiggingCommand : ICommand List _originalGeometries; List _giveAnimationToList; - Rmv2MeshNode _takeAnimationFrom; + List _takeAnimationFromList; public string HintText { get => "Skin wrap re-rigging"; } public bool IsMutation { get => true; } - - public SkinWrapRiggingCommand(SelectionManager selectionManager) { - _selectionManager = selectionManager; ; + _selectionManager = selectionManager; } - public void Configure(IEnumerable giveAnimationTo, Rmv2MeshNode takeAnimationFrom) + public void Configure(IEnumerable giveAnimationTo, List takeAnimationFrom) { _giveAnimationToList = giveAnimationTo.ToList(); - _takeAnimationFrom = takeAnimationFrom; + _takeAnimationFromList = takeAnimationFrom; } public void Execute() { - // Create undo state _originalGeometries = _giveAnimationToList.Select(x => x.Geometry.Clone()).ToList(); _selectionOldState = _selectionManager.GetStateCopy(); - // Update the meshes + var firstSource = _takeAnimationFromList[0]; foreach (var giveAnimationTo in _giveAnimationToList) { - // Set skeleton and vertex type from first source object - giveAnimationTo.Geometry.ChangeVertexType(_takeAnimationFrom.Geometry.VertexFormat, false); - giveAnimationTo.Geometry.UpdateSkeletonName(_takeAnimationFrom.Geometry.SkeletonName); + giveAnimationTo.Geometry.ChangeVertexType(firstSource.Geometry.VertexFormat, false); + giveAnimationTo.Geometry.UpdateSkeletonName(firstSource.Geometry.SkeletonName); + + var maxBoneInfluences = giveAnimationTo.Geometry.WeightCount; for (var i = 0; i < giveAnimationTo.Geometry.VertexCount(); i++) { - var inputVertexPos = giveAnimationTo.Geometry.VertexArray[i].Position3(); - var res = RegiggingHelper.FindClosestUV(inputVertexPos, _takeAnimationFrom.Geometry, _takeAnimationFrom.Position); + var localVertexPos = giveAnimationTo.Geometry.VertexArray[i].Position3(); + var worldVertexPos = localVertexPos + giveAnimationTo.Position; + + var result = RegiggingHelper.FindClosestBoneWeightsMultiMesh(worldVertexPos, _takeAnimationFromList, maxBoneInfluences); - giveAnimationTo.Geometry.VertexArray[i].BlendIndices = res.Bones; - giveAnimationTo.Geometry.VertexArray[i].BlendWeights = res.BlendWeights; + giveAnimationTo.Geometry.SetVertexBlendIndex(i, result.BoneIndices); + giveAnimationTo.Geometry.SetVertexWeights(i, result.BlendWeights); } giveAnimationTo.Geometry.RebuildVertexBuffer(); diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs index 3219e1133..ba37a04d3 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/PinToolViewModel.cs @@ -26,7 +26,6 @@ public partial class PinToolViewModel : ObservableObject [ObservableProperty] RiggingMode[] _possibleRiggingModes = Enum.GetValues(); [ObservableProperty] RiggingMode _selectedRiggingMode = RiggingMode.Pin; [ObservableProperty] ObservableCollection _affectedMeshCollection = []; - [ObservableProperty] ObservableCollection _sourceMeshCollection = []; public PinToolViewModel(SelectionManager selectionManager, CommandFactory commandFactory, IStandardDialogs standardDialogs) { @@ -87,7 +86,7 @@ public bool Apply() return PinMode.Execute(AffectedMeshCollection.ToList()); case RiggingMode.SkinWrap: - return SkinWrapMode.Excute(AffectedMeshCollection.ToList()); + return SkinWrapMode.Execute(AffectedMeshCollection.ToList()); default: throw new NotImplementedException($"unable to find an algorithm for selected mode '{SelectedRiggingMode}'"); } diff --git a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml index 7ad2527a6..c4202701a 100644 --- a/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml +++ b/Editors/Kitbashing/KitbasherEditor/ChildEditors/PinTool/Presentation/PinToolWindow.xaml @@ -10,29 +10,10 @@ mc:Ignorable="d" Style="{StaticResource CustomWindowStyle}" + HelpDocumentPath="Documentation\AssetEditorDocumentation.html?doc=Kitbash_PinTool" Title="{loc:Loc KitbashTool.PinTool.Title}" Height="600" Width="400"> - - - 1. Select mode: - - 'Pin': for things which should not bend - - 'Skin wrap': for things which should deform - - 2. Select meshes which should get new animation - - 3. Select the meshes to take animation from - - 'Pin': go into vertex mode and press button with a vertex selected - - 'Skin wrap': select mesh - - 4. Press Apply - - -