s&box Package Code Search
Search C# source code, UI razor templates, shaders, and configs across s&box packages.
link Raw Facepunch API Link: https://public.facepunch.com/sbox/code/search/1/?ident=notpointless.chomnr_humanoid_rigger&take=20
Showing code results for query:
*
(110 total matches found)
Editor
library
using Editor;
using Sandbox;
using System.Threading.Tasks;
namespace HumanoidRigger.Editor;
public sealed record ExportResult(string[] Files)
{
public string PrimaryFile=>Files.LastOrDefault(p=>p.EndsWith(".vmdl",StringComparison.OrdinalIgnoreCase))??Files.First();
}
public static class AssetOutput
{
public static async Task<string> Save(Wizard session)=>
(await Save(session,ExportRequest.Default(session.Character!.Name,Project.Current.GetAssetsPath(),session.Character.Materials.Length>0))).PrimaryFile;
public static async Task<ExportResult> Save(Wizard session,ExportRequest request)
{
if(session.Rig?.Report.Passed!=true)throw new InvalidOperationException("The rig must pass validation before saving.");
var character=session.Character!;var rig=session.Rig;
if(request.Formats.HasFlag(ExportFormats.Vmdl))VmdlBoneNames.Validate(rig.Bones);
var root=Project.Current.GetAssetsPath();var plan=request.Plan(root,character.Materials.Length>0);plan.EnsureAvailable();
Directory.CreateDirectory(plan.Directory);
var written=new List<string>();bool createdMaterials=false;
void Write(string path,byte[] bytes)
{
using var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read);written.Add(path);file.Write(bytes);
}
void WriteText(string path,string content)=>Write(path,System.Text.Encoding.UTF8.GetBytes(content));
try
{
var sourceMaterials=character.Materials;
IReadOnlyDictionary<int,string> vmats=new Dictionary<int,string>();
if(plan.MaterialDirectory is not null)
{
Directory.CreateDirectory(plan.MaterialDirectory);createdMaterials=true;
sourceMaterials=await Task.Run(()=>TextureFiles.Copy(character,plan.MaterialDirectory));await new EditorThread();
sourceMaterials=await Task.Run(()=>MaterialAssets.PrepareFormats(sourceMaterials,plan.MaterialDirectory,plan.Formats));await new EditorThread();
if(plan.Formats.HasFlag(ExportFormats.Vmdl))
{
vmats=await MaterialAssets.Compile(sourceMaterials,plan.MaterialDirectory,character);
}
}
var portableMaterials=plan.MaterialDirectory is null?sourceMaterials:TextureFiles.RelativeTo(sourceMaterials,Path.GetFileName(plan.MaterialDirectory));
if(plan.Formats.HasFlag(ExportFormats.Fbx))
{
var bytes=await Task.Run(()=>FbxExporter.Write(character,rig,portableMaterials));await new EditorThread();
Write(plan.PathFor(".fbx"),bytes);
if(ExportRequest.IsInAssets(plan.Directory,root))AssetSystem.RegisterFile(plan.PathFor(".fbx"));
}
if(plan.Formats.HasFlag(ExportFormats.Gltf))
{
var output=await Task.Run(()=>GltfExporter.Write(character,rig,plan.FileName+".bin",portableMaterials));await new EditorThread();
Write(plan.PathFor(".bin"),output.Buffer);Write(plan.PathFor(".gltf"),output.Document);
}
if(plan.Formats.HasFlag(ExportFormats.Glb))
{
var output=await Task.Run(()=>GltfExporter.Write(character,rig,"",portableMaterials,true,path=>File.ReadAllBytes(Path.Combine(plan.Directory,path))));await new EditorThread();
Write(plan.PathFor(".glb"),output.Document);
}
if(plan.Formats.HasFlag(ExportFormats.Obj))
{
var output=await Task.Run(()=>ObjExporter.Write(character,plan.FileName+".mtl",portableMaterials));await new EditorThread();
WriteText(plan.PathFor(".mtl"),output.Materials);WriteText(plan.PathFor(".obj"),output.Mesh);
}
if(plan.Formats.HasFlag(ExportFormats.Vmdl))
{
var dmx=await Task.Run(()=>DmxExporter.Write(character,rig,vmats));await new EditorThread();
WriteText(plan.PathFor(".dmx"),dmx);
WriteText(plan.PathFor(".vmdl"),ModelDocExporter.Write(Path.GetRelativePath(root,plan.PathFor(".dmx")),rig,character,vmats.Count>0));
await NativeRigExport.Compile(plan.PathFor(".dmx"),plan.PathFor(".vmdl"),character,rig,vmats,
content=>File.WriteAllText(plan.PathFor(".dmx"),content));
}
return new(plan.PrimaryFiles);
}
catch
{
// Only remove output paths this attempt created. Existing user assets are never overwritten.
foreach(var path in written)foreach(var owned in new[]{path,path+"_c"})try{File.Delete(owned);}catch(Exception e){Log.Warning(e.Message);}
if(createdMaterials&&ExportRequest.IsInAssets(plan.MaterialDirectory,plan.Directory))
try{Directory.Delete(plan.MaterialDirectory,true);}catch(Exception e){Log.Warning(e.Message);}
throw;
}
}
}
Editor
library
using Editor;
using Sandbox;
using System.Threading.Tasks;
namespace HumanoidRigger.Editor;
public sealed class RiggerWindow : Widget
{
public const string DockTitle="Humanoid Rigger";
public static RiggerWindow Instance {get;private set;}
public Wizard Session {get;}=new();
RiggerViewport viewport;Widget content,toolbar,footer,toolbarContent,footerContent;Label status;bool busy;
ImportedCharacter framedCharacter;WizardStep framedStep;
JointPanel jointPanel;
CharacterPose previewPose=CharacterPose.Auto;
ComboBox poseSelection;
bool customPose;
bool updatingPoseSelection;
IReadOnlyDictionary<string,System.Numerics.Quaternion> editedPose;
AutomaticHandRefiner handRefiner;
IReadOnlyDictionary<int,string> previewMaterials=new Dictionary<int,string>();
internal double LastAdvanceWorkMilliseconds {get;private set;}
internal double LastAdvanceUiMilliseconds {get;private set;}
internal bool LastAdvanceWorkerWasMainThread {get;private set;}
Task<Wizard> preparation;
Wizard preparingDraft;
long queuedPreparationRevision=-1;
FingerCountDialog fingerCountDialog;
public RiggerWindow(Widget parent):this(parent,true){}
internal RiggerWindow(Widget parent,bool register):base(parent)
{
if(register)Instance=this;WindowTitle=DockTitle;Name="HumanoidRigger";Cursor=CursorShape.Arrow;SetWindowIcon("accessibility_new");MinimumSize=new(1060,780);Size=new(1280,940);Layout=Layout.Column();EnsureHandRefiner();Build();
}
[Event("tools.editorwindow.createview")]
static void RegisterViewMenu(Menu menu)
{
// Join the editor's alphabetically sorted tools without creating a dock.
EditorWindow.DockManager.RegisterDockType(new DockManager.DockInfo
{
Title=DockTitle,Icon="accessibility_new",CreateAction=OpenFloatingView
});
}
static Widget OpenFloatingView(){Open();return null;}
[Event("tools.editorwindow.postcreateview")]
static void ConfigureViewMenu(Menu menu)
{
var option=menu.GetOption(DockTitle);
if(option is null)return;
option.Toggled=null;option.Checkable=false;option.Triggered=Open;
}
public static void Open()
{
if(Instance.IsValid()){ShowExistingWindow(Instance);return;}
CreateFloatingWindow(DockTitle,true);
}
internal static void ShowExistingWindow(RiggerWindow window)
{
// Move sessions opened by older versions into the same themed window as new
// sessions. Preserve the widget, viewport and edits when removing the old dock.
var dock=EditorWindow.DockManager.FindDockWidget(window);
if(dock.IsValid())
{
var previous=window.GetWindow();var size=previous.Size;var position=previous.Position;bool floating=dock.IsFloating;
var dialog=CreateFloatingHost(window.WindowTitle);
// Replacing the dock content releases its native ownership before reparenting.
dock.Widget=new Widget(dock);
window.Parent=dialog;dialog.Layout.Add(window,1);
EditorWindow.DockManager.RemoveDock(dock);dock.Destroy();
if(floating){dialog.Window.Size=size;dialog.Window.Position=position;}
dialog.Show();window.Show();dialog.Window.Raise();
return;
}
var existing=window.GetWindow();existing.Show();window.Show();existing.Raise();
}
internal static Dialog CreateFloatingWindow(string title,bool register)
{
var dialog=CreateFloatingHost(title);
dialog.Layout.Add(new RiggerWindow(dialog,register),1);dialog.Show();dialog.Window.Size=new(1280,940);return dialog;
}
void Build()
{
if(Session.Rig is null){previewPose=CharacterPose.Auto;customPose=false;editedPose=null;}
if(Session.Step==WizardStep.Import)
{
content?.Destroy();viewport=null;framedCharacter=null;
content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();
var drop=new ModelDropArea(content,ImportPath);content.Layout.Add(drop,1);status=content.Layout.Add(new Label(content){Visible=false,WordWrap=true});return;
}
if(!viewport.IsValid()||!toolbar.IsValid()||!footer.IsValid()||!jointPanel.IsValid())
{
if(viewport.IsValid())viewport.Parent=this;
content?.Destroy();content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();
toolbar=content.Layout.Add(new Widget(content));toolbar.Layout=Layout.Column();
var middle=content.Layout.Add(new Widget(content),1);middle.Layout=Layout.Column();
viewport=middle.Layout.Add(viewport.IsValid()?viewport:new RiggerViewport(middle,Session),1);
// Overlay the panel so entering Body does not resize the native
// render surface. The viewport frames the character beside it.
jointPanel=new JointPanel(viewport,Session,viewport);viewport.Changed=LandmarksChanged;
// Reserve the final review's space throughout the wizard. Changing
// warnings or progress text must not repeatedly resize the swap chain.
footer=content.Layout.Add(new Widget(content){FixedHeight=160});footer.Layout=Layout.Column();
}
viewport.PoseEdited=PoseEdited;
viewport.Resized=PositionJointPanel;
toolbarContent?.Destroy();footerContent?.Destroy();
toolbarContent=toolbar.Layout.Add(new Widget(toolbar));toolbarContent.Layout=Layout.Row();
footerContent=footer.Layout.Add(new Widget(footer));footerContent.Layout=Layout.Column();
var top=toolbarContent.Layout;top.Margin=8;top.Spacing=8;
top.Add(new Label(content){Text="Profile:"});
var profile=top.Add(new ComboBox(content){MinimumWidth=190,ToolTip="The target skeleton’s bone names and hierarchy. Hand detection is independent of this choice."});
foreach(var p in Profiles.BuiltIn.Concat(ProfileStore.Load()))profile.AddItem(p.Name,"person",()=>{Session.SetProfile(p);Build();},selected:p.Id==Session.Profile.Id);
profile.AddItem("Create Custom Profile…","add",CreateProfile);
profile.AddItem("Load Profile…","folder_open",LoadProfile);
if(Session.Rig is not null)
{
top.Add(new Label(content){Text="Preview:"});
poseSelection=top.Add(new ComboBox(content){MinimumWidth=120,ToolTip="Preview the generated rig in a standard pose."});
foreach(var (label,value) in new[]{("Original Pose",CharacterPose.Auto),("T-Pose",CharacterPose.TPose),("A-Pose 1",CharacterPose.APose1),("A-Pose 2",CharacterPose.APose2)})
poseSelection.AddItem(label,"accessibility",()=>SetPreviewPose(value),selected:!customPose&&value==previewPose);
if(editedPose is not null)poseSelection.AddItem("Custom Pose","touch_app",ShowEditedPose,selected:customPose);
}
top.AddStretchCell();top.Add(new Button("Replace model","folder_open"){Clicked=SelectModel});top.Add(new Button("Restart","restart_alt"){Clicked=RestartWorkflow});
viewport.MaterialPaths=previewMaterials;ShowPreview();
jointPanel.Visible=Session.Step!=WizardStep.Centerline;
PositionJointPanel();
jointPanel.Reload();
if(framedCharacter!=Session.Character||framedStep!=Session.Step){viewport.Frame();framedCharacter=Session.Character;framedStep=Session.Step;}
var bottom=footerContent.Layout;bottom.Margin=12;bottom.Spacing=8;
status=bottom.Add(new Label(content){WordWrap=true,Text=Instruction()});
if(Session.Step is WizardStep.Centerline or WizardStep.Body)
{
var importWarnings=Session.Character.ImportWarnings.Where(w=>!w.StartsWith("Detected a Z-up")).ToArray();
if(importWarnings.Length>0)
{
var warning=bottom.Add(new Label(content){Name="ImportMaterialWarning",Text=string.Join("\n",importWarnings),WordWrap=true});
warning.SetStyles($"color: {Theme.Yellow.Hex};");
}
}
if(Session.Step is WizardStep.Centerline or WizardStep.Body && Session.Anatomy!.UnrecommendedImportPose)
{
var warning=bottom.Add(new Label(content){Name="ImportPoseWarning",Text=ImportPose.Warning,WordWrap=true});
warning.SetStyles($"color: {Theme.Yellow.Hex};");
}
if(Session.Step is WizardStep.LeftHand or WizardStep.RightHand)
{
var side=Session.Step==WizardStep.LeftHand?"L":"R";
foreach(var warning in Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+" hand")||w.StartsWith(side+" fingers")))
{var label=bottom.Add(new Label(content){Text=warning,WordWrap=true});label.SetStyles($"color: {Theme.Yellow.Hex};");}
}
if(Session.Step==WizardStep.Finish)
{
bottom.Add(new Label(content){Text=$"Profile: {Session.Profile.Name}"});
var deformationWarnings=Session.Rig!.Report.Issues.Where(i=>i.Code=="surface-reversal").Select(i=>i.Message).ToArray();
if(deformationWarnings.Length>0)
{
var warning=bottom.Add(new Label(content){Name="DeformationWarning",Text="Some test poses need review. Open Advanced Edit for details.",WordWrap=true});
warning.SetStyles($"color: {Theme.Yellow.Hex};");
}
var checks=bottom.AddRow();checks.Spacing=8;
foreach(var label in new[]{"Body","Left Hand","Right Hand","Skeleton","Skinning","Validation"})
{
string side=label=="Left Hand"?"L":label=="Right Hand"?"R":null;
var warnings=label=="Validation"?deformationWarnings:side is null?[]:Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+" hand")||w.StartsWith(side+" fingers")).ToArray();
checks.Add(new StatusChip(content,label,warnings.Length>0?Theme.Yellow:Theme.Green){ToolTip=warnings.Length>0?string.Join("\n",warnings):"Checked"});
}
checks.AddStretchCell();
var row=bottom.AddRow();row.Spacing=8;
row.Add(new Button("Back","arrow_back"){Clicked=GoBack});
row.Add(new Button("Reset Pose","restart_alt"){Clicked=()=>SetPreviewPose(CharacterPose.Auto)});
row.Add(new Button("Test Rig","play_arrow"){Clicked=TestRig});row.Add(new Button("Advanced Edit","tune"){Clicked=Advanced});row.AddStretchCell();row.Add(new Button.Primary("Save"){Icon="check",Tint=Theme.Green,Clicked=Save});
}
else
{
var row=bottom.AddRow();row.Spacing=8;
if(Session.Step!=WizardStep.Centerline)row.Add(new Button("Back","arrow_back"){Clicked=GoBack});
row.Add(new Button("Reset","restart_alt"){Clicked=()=>{Session.Reset();Build();}});row.AddStretchCell();
row.Add(new Button.Primary("Continue"){Enabled=Session.Step!=WizardStep.Validation,Clicked=Continue});
}
SchedulePreparation();
}
void PositionJointPanel()
{
if(!viewport.IsValid()||!jointPanel.IsValid())return;
jointPanel.Position=new(viewport.Width-jointPanel.FixedWidth,0);
jointPanel.Size=new(jointPanel.FixedWidth,viewport.Height);
viewport.RightInset=jointPanel.Visible?jointPanel.FixedWidth:0;
jointPanel.Raise();
}
void GoBack(){Session.Back();Build();}
string Instruction()=>Session.Step switch
{
WizardStep.Centerline=>"Check the centerline.\nDrag the line left or right to adjust it.",
WizardStep.Body=>"Check the points.\nMove any point that is incorrect.",WizardStep.LeftHand=>"Check the left hand.\nMove any incorrect points.",WizardStep.RightHand=>"Check the right hand.\nMove any incorrect points.",WizardStep.Finish=>"Rig Complete\nDrag a bone to test the rig.",
WizardStep.Validation=>string.Join("\n",Session.Rig!.Report.Issues.Where(i=>i.Error).Select(i=>i.Message)),_=>"Generating rig…"
};
void SelectModel(){var path=EditorUtility.OpenFileDialog("Select model",ModelImporter.FileFilter,"");if(!string.IsNullOrEmpty(path))ImportPath(path);}
void CreateProfile()
{
var path=EditorUtility.OpenFileDialog("Select rigged character","Rigged characters (*.fbx *.gltf *.glb)","");if(string.IsNullOrEmpty(path))return;
try{new ProfileDialog(this,ModelImporter.Import(path),p=>{Session.SetProfile(p);Build();}).Show();}catch(Exception e){Error(e);}
}
void LoadProfile()
{
var path=EditorUtility.OpenFileDialog("Select rig profile","json","");if(string.IsNullOrEmpty(path))return;
try{var p=RigProfile.FromJson(File.ReadAllText(path));ProfileStore.Save(p);Session.SetProfile(p);Build();}catch(Exception e){Error(e);}
}
public async void ImportPath(string path)
{
try{await ImportAsync(path);}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}
}
public async Task ImportAsync(string path)
{
if(busy)throw new InvalidOperationException("The current operation is still running.");
EnsureHandRefiner();
SetBusy(true);
try
{
await RigWork.Run(()=>Session.Import(path));await new EditorThread();
if(!this.IsValid())return;
previewMaterials=new Dictionary<int,string>();
string textureWarning=null;
try{previewMaterials=await MaterialAssets.Preview(Session.Character);}
catch(Exception e){textureWarning="Textures: "+e.Message;Log.Warning(textureWarning);}
await new EditorThread();if(this.IsValid()){Build();if(textureWarning is not null){status.Text+="\n"+textureWarning;status.SetStyles($"color: {Theme.Yellow.Hex};");}}
}
finally{await new EditorThread();SetBusy(false);}
}
void Continue()
{
if(busy)return;
if(Session.Step==WizardStep.Body)
{
if(fingerCountDialog.IsValid()&&fingerCountDialog.Visible){fingerCountDialog.Window.Raise();return;}
var revision=Session.Revision;
fingerCountDialog=new FingerCountDialog(this,Session.LeftFingerCount,Session.RightFingerCount,(left,right)=>
{
if(!this.IsValid()||Session.Step!=WizardStep.Body||Session.Revision!=revision)return;
Session.SetFingerCounts(left,right);AdvanceFromControls();
});
fingerCountDialog.Show();return;
}
AdvanceFromControls();
}
async void AdvanceFromControls()
{
try{await AdvanceAsync();}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}
}
public async Task AdvanceAsync()
{
if(busy)throw new InvalidOperationException("The current operation is still running.");
EnsureHandRefiner();
SetBusy(true);
status.Text=Session.Step switch{WizardStep.Body=>"Preparing left hand…",WizardStep.LeftHand=>"Preparing right hand…",WizardStep.RightHand=>"Generating rig…",_=>Instruction()};
try
{
var started=System.Diagnostics.Stopwatch.GetTimestamp();
if(preparation is not null&&!preparation.IsCompleted&&!Session.CanAccept(preparingDraft))
{try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}
Wizard result;
if(Session.Step==WizardStep.RightHand)
{
// Give the busy state a frame to appear, then generate on the
// editor thread. Keep a draft so a failure preserves the edits.
await Task.Delay(16).ConfigureAwait(false);await new EditorThread();
if(!this.IsValid())return;
result=Session.CopyForContinuation();
LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;
result.Continue();
}
else
{
if(preparation is null||!Session.CanAccept(preparingDraft)||preparation.IsFaulted)StartPreparation();
result=await preparation.ConfigureAwait(false);await new EditorThread();
}
Session.AcceptContinuation(result);
LastAdvanceWorkMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
started=System.Diagnostics.Stopwatch.GetTimestamp();if(this.IsValid())Build();
LastAdvanceUiMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;
}
finally{await new EditorThread();SetBusy(false);SchedulePreparation();}
}
public void CaptureViewport(string path)=>viewport.Capture(path);
internal void RefreshDisplay()
{
// Rebind native event handlers after editor hot reload without replacing the session.
viewport?.Destroy();viewport=null;framedCharacter=null;MinimumSize=new(1060,780);Build();
}
void ShowPreview()
{
if(Session.Rig is null){viewport.ShowCharacter();return;}
viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose),false);
}
public void SetPreviewPose(CharacterPose pose)
{
if(updatingPoseSelection)return;
if(busy)throw new InvalidOperationException("The current operation is still running.");
if(Session.Rig is null)throw new InvalidOperationException("Generate the rig before previewing poses.");
if(pose is not (CharacterPose.Auto or CharacterPose.TPose or CharacterPose.APose1 or CharacterPose.APose2))throw new ArgumentException("Unsupported preview pose.");
previewPose=pose;customPose=false;viewport.SetPose(RigPosePreview.Rotations(Session.Rig,pose));
var label=pose switch{CharacterPose.Auto=>"Original Pose",CharacterPose.TPose=>"T-Pose",CharacterPose.APose1=>"A-Pose 1",_=>"A-Pose 2"};
SelectPoseLabel(label);
status.Text=Instruction();
}
void PoseEdited(IReadOnlyDictionary<string,System.Numerics.Quaternion> pose)
{
customPose=true;editedPose=new Dictionary<string,System.Numerics.Quaternion>(pose);
SelectPoseLabel("Custom Pose");
}
void SelectPoseLabel(string label)
{
if(!poseSelection.IsValid())return;
// Native ComboBox selection invokes its action even for programmatic
// changes. Updating the label must not restart or cancel an active drag.
updatingPoseSelection=true;
try
{
if(poseSelection.FindIndex(label) is {} index)poseSelection.CurrentIndex=index;
else if(label=="Custom Pose")poseSelection.AddItem(label,"touch_app",ShowEditedPose,selected:true);
}
finally{updatingPoseSelection=false;}
}
void ShowEditedPose(){if(updatingPoseSelection||editedPose is null||Session.Rig is null)return;customPose=true;viewport.SetPose(editedPose);}
public void RestartWorkflow()
{
if(busy)throw new InvalidOperationException("The current operation is still running.");
Session.Restart();Build();
}
void Error(Exception e){status.Visible=true;status.Text=e.Message;status.SetStyles($"color: {Theme.Red.Hex};");}
void SetBusy(bool value)
{
busy=value;if(!this.IsValid())return;
if(viewport.IsValid())
{
viewport.AllowEditing=!value;
if(toolbar.IsValid())toolbar.Enabled=!value;if(footer.IsValid())footer.Enabled=!value;if(jointPanel.IsValid())jointPanel.Enabled=!value;
}
else if(content.IsValid())content.Enabled=!value;
}
async void TestRig()
{
if(busy)return;SetBusy(true);
try
{
foreach(var pose in Deformation.Poses)
{
await new EditorThread();if(!this.IsValid()||Session.Rig is null)break;
if(!Deformation.IsApplicable(pose,Session.Rig.Bones.Select(b=>b.Role).ToHashSet()))continue;
viewport.SetPose(Deformation.JointRotations(Session.Rig,pose));status.Text=pose.Name;await Task.Delay(650);
}
await new EditorThread();if(this.IsValid()){viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose));status.Text=Instruction();}
}
catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}finally{await new EditorThread();SetBusy(false);}
}
void Advanced()
{
var dialog=new Dialog(this);dialog.Window.WindowTitle="Rig details";dialog.Window.MinimumSize=new(560,480);dialog.Layout=Layout.Column();dialog.Layout.Margin=12;dialog.Layout.Spacing=8;
var scroll=new ScrollArea(dialog);scroll.Canvas=new Widget(scroll);scroll.Canvas.Layout=Layout.Column();
foreach(var b in Session.Rig!.Bones)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$"{b.Name} · {b.Role} · {b.Position}"});
foreach(var issue in Session.Rig.Report.Issues)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=issue.Message});
foreach(var hand in Session.Anatomy!.HandRefinements)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$"{(hand.Key=="L"?"Left":"Right")} hand: {hand.Value.Status}"});
dialog.Layout.Add(scroll,1);dialog.Show();
}
void Save()
{
if(busy)return;
new SaveRigDialog(this,Session,result=>{if(this.IsValid()){status.Visible=true;status.Text="Saved "+string.Join(" + ",result.Files.Select(Path.GetFileName));status.ToolTip=string.Join("\n",result.Files);status.SetStyles($"color: {Theme.Green.Hex};");}}).Show();
}
void EnsureHandRefiner(){if(handRefiner is null){handRefiner=new AutomaticHandRefiner();handRefiner.Warmup();}Session.HandRefiner=handRefiner;}
public override void OnDestroyed(){handRefiner?.Dispose();if(Instance==this)Instance=null;base.OnDestroyed();}
static Dialog CreateFloatingHost(string title)
{
var dialog=new Dialog(null);dialog.Window.Title=title;dialog.Window.SetWindowIcon("accessibility_new");
dialog.Layout=Layout.Column();dialog.Window.Size=new(1280,940);return dialog;
}
void LandmarksChanged(){jointPanel.RefreshRows();SchedulePreparation();}
void StartPreparation()
{
preparingDraft=Session.CopyForContinuation();var draft=preparingDraft;
preparation=RigWork.Run(()=>{LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;draft.Continue();return draft;});
_=preparation.ContinueWith(task=>{_ = task.Exception;},TaskContinuationOptions.OnlyOnFaulted);
}
void SchedulePreparation()
{
// Preparing hands is cheap; generating a full rig while its final hand
// is still being edited wastes both memory and a complete repair pass.
if(!this.IsValid()||Session.Step is not (WizardStep.Body or WizardStep.LeftHand)||queuedPreparationRevision==Session.Revision)return;
queuedPreparationRevision=Session.Revision;_=PrepareWhenIdle(Session.Revision);
}
async Task PrepareWhenIdle(long revision)
{
// Coalesce marker drags. At most one solver job per window may run at a time.
await Task.Delay(150).ConfigureAwait(false);await new EditorThread();
if(!this.IsValid()||busy||Session.Revision!=revision)return;
if(preparation is not null&&!preparation.IsCompleted)
{try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}
if(!this.IsValid()||busy||Session.Revision!=revision)return;
if(preparingDraft is null||!Session.CanAccept(preparingDraft))StartPreparation();
}
}
sealed class ModelDropArea:Widget
{
readonly Action<string> import;int hover;
public ModelDropArea(Widget parent,Action<string> import):base(parent)
{
this.import=import;AcceptDrops=true;Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;Layout.AddStretchCell();
var row=Layout.AddRow();row.AddStretchCell();var center=row.AddColumn();center.Spacing=12;
center.Add(new DropFolderIcon(this));
center.Add(new Label(this){Text="Please drag and drop a character file here (.fbx, .obj, .gltf, .glb)",Alignment=TextFlag.Center});
center.Add(new Label(this){Text="or",Alignment=TextFlag.Center});
var choice=center.AddRow();choice.AddStretchCell();
choice.Add(new Button.Primary("Choose File"){MinimumWidth=120,Clicked=()=>{var path=EditorUtility.OpenFileDialog("Choose File",ModelImporter.FileFilter,"");if(!string.IsNullOrEmpty(path))import(path);}});
choice.AddStretchCell();
row.AddStretchCell();Layout.AddStretchCell();
}
public override void OnDragHover(DragEvent e)
{
bool valid=e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder);hover=valid?1:-1;if(valid)e.Action=DropAction.Link;Update();
}
public override void OnDragDrop(DragEvent e){hover=0;if(e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder)){e.Action=DropAction.Link;import(e.Data.FileOrFolder);}Update();}
public override void OnDragLeave(){hover=0;Update();}
protected override void OnPaint(){Paint.SetPen(hover==1?Theme.Green:hover<0?Theme.Red:Theme.ControlBackground.Lighten(.2f),1);Paint.SetBrush(hover==1?Theme.Green.WithAlpha(.06f):Paint.HasMouseOver?Theme.ControlBackground.Lighten(.3f):Theme.ControlBackground);Paint.DrawRect(LocalRect.Shrink(12),4);}
}
sealed class DropFolderIcon : Widget
{
public DropFolderIcon(Widget parent):base(parent){FixedHeight=48;}
protected override void OnPaint()
{
Paint.SetPen(Theme.TextLight);
Paint.DrawIcon(new Rect((Width-40)*.5f,4,40,40),"create_new_folder",40);
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>A low, narrow neck beneath an enlarged head provides a body-scale
/// prior independent of total height. Ambiguous sections retain the original prior.</summary>
internal static class BodyProportions
{
readonly record struct Section(float Y,float Width,float Depth)
{
public float Area=>Width*Depth;
}
public static float EstimateBodyHeight(IEnumerable<MeshPart> source,float bottom,float height)
{
var meshes=source.ToArray();var sections=new List<Section>();
// Intersect triangle edges rather than sampling vertices: dense faces,
// sparse neck rings and material seams should give the same cross-section.
for(int sample=0;sample<=128;sample++)
{
float y=bottom+height*(.5f+sample*.0035f);
float minX=float.PositiveInfinity,maxX=float.NegativeInfinity,minZ=float.PositiveInfinity,maxZ=float.NegativeInfinity;
int count=0;
foreach(var mesh in meshes)for(int t=0;t<mesh.Triangles.Length;t+=3)for(int edge=0;edge<3;edge++)
{
var a=mesh.Vertices[mesh.Triangles[t+edge]];var b=mesh.Vertices[mesh.Triangles[t+(edge+1)%3]];
if(!((a.Y<=y&&b.Y>y)||(b.Y<=y&&a.Y>y)))continue;
var p=Vector3.Lerp(a,b,(y-a.Y)/(b.Y-a.Y));
minX=Math.Min(minX,p.X);maxX=Math.Max(maxX,p.X);minZ=Math.Min(minZ,p.Z);maxZ=Math.Max(maxZ,p.Z);count++;
}
if(count>=4&&maxX-minX>height*.005f&&maxZ-minZ>height*.005f)sections.Add(new(y,maxX-minX,maxZ-minZ));
}
var candidates=sections.Where(s=>s.Y<bottom+height*.9f).ToArray();
if(candidates.Length==0)return height;
var narrowest=candidates.MinBy(s=>s.Area);
if(narrowest.Y>=bottom+height*.8f)return height;
int index=sections.IndexOf(narrowest),first=index,last=index;
bool SameNeck(Section a,Section b)=>b.Area<=narrowest.Area*2.5f&&Math.Abs(a.Y-b.Y)<=height*.0071f;
while(first>0&&SameNeck(sections[first],sections[first-1]))first--;
while(last+1<sections.Count&&SameNeck(sections[last],sections[last+1]))last++;
if(last-first<2)return height;
float neckY=(sections[first].Y+sections[last].Y)*.5f;
// An unusually narrow waist is not the neck if another bottleneck
// separates the shoulders and skull farther up the same silhouette.
foreach(var later in sections.Where(s=>s.Y>neckY+height*.1f&&s.Y<bottom+height*.9f))
{
bool Expanded(Section s)=>s.Width>later.Width*1.35f&&s.Depth>later.Depth*1.1f;
if(sections.Any(s=>s.Y<later.Y-height*.02f&&s.Y>later.Y-height*.07f&&Expanded(s))&&
sections.Any(s=>s.Y>later.Y+height*.02f&&s.Y<later.Y+height*.07f&&Expanded(s)))return height;
}
// Require expansion in both transverse dimensions above the neck. An arm
// silhouette or a narrow waist alone is not sufficient evidence of a head.
if(!sections.Any(s=>s.Y>Math.Max(neckY+height*.025f,bottom+height*.82f)&&s.Width>narrowest.Width*2.2f&&s.Depth>narrowest.Depth*1.5f))return height;
return Math.Min(height,(neckY-bottom)/.85f);
}
}
Editor
library
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Refine a high hip from the thigh centerline and its junction with
/// the pelvis. Ambiguous branches and large changes retain the original prior.</summary>
internal static class HipFitting
{
public static void RaiseLowHips(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)
{
var old=new[]{anatomy.Points["UpperLeg.L"],anatomy.Points["UpperLeg.R"]};
if(old.Any(p=>p.Corrected))return;
var candidates=new Vector3[2];float center=anatomy.SymmetryPlaneX;
for(int i=0;i<2;i++)
{
float sign=i==0?1:-1;var hip=old[i].Position;
var leg=sections.Where(s=>(s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.06f&&
s.Center.Y>hip.Y-height*.07f&&s.Center.Y<hip.Y+height*.15f&&s.Radius<height*.095f).ToArray();
var top=leg.MaxBy(s=>s.Center.Y);
if(top is null||top.Center.Y<hip.Y+height*.015f||top.MinimumRadius<height*.012f)return;
// Two separate thigh contours must actually join a central pelvis.
if(!sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y>top.Center.Y&&s.Center.Y<top.Center.Y+height*.02f&&s.Area>top.Area*1.5f))return;
// A torso contour already overlapping the thigh is a separate shell,
// not evidence of a groin transition. Keep its anatomical prior.
if(sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y<top.Center.Y&&s.Center.Y>top.Center.Y-height*.03f&&s.Area>top.Area*1.5f))return;
var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.015f&&s.Center.Y>top.Center.Y-height*.06f).ToArray();if(shaft.Length<4)return;
// A narrowing end cap belongs to a detached leg segment. Extending
// its radius would place the socket beyond its authored articulation.
if(top.MinimumRadius<shaft.Average(s=>s.MinimumRadius)*.85f)return;
var mean=Geometry.Mean(shaft.Select(s=>s.Center));float variance=shaft.Sum(s=>(s.Center.Y-mean.Y)*(s.Center.Y-mean.Y));if(variance<height*height*1e-8f)return;
var slope=shaft.Aggregate(Vector3.Zero,(sum,s)=>sum+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;
var candidate=mean+slope*(top.Center.Y+top.MinimumRadius-mean.Y);
if(Vector3.Distance(candidate,hip)>height*.14f||!volume.Contains(candidate,height*.00001f))return;
candidates[i]=candidate;
}
if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(old[0].Position.Y-old[1].Position.Y)+height*.01f)return;
var pelvis=anatomy.Points["Pelvis"];float lift=(candidates[0].Y+candidates[1].Y-old[0].Position.Y-old[1].Position.Y)*.5f;
var moved=pelvis.Position+Vector3.UnitY*lift;
if(!pelvis.Corrected&&volume.Contains(moved,height*.00001f))anatomy.Points["Pelvis"]=pelvis with{Position=moved};
for(int i=0;i<2;i++)anatomy.Points[old[i].Role]=old[i] with{Position=candidates[i]};
}
public static void Refine(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)
{
float center=anatomy.SymmetryPlaneX;
var original=new[]{anatomy.Points["UpperLeg.L"],anatomy.Points["UpperLeg.R"]};
var candidates=original.Select(p=>p.Position).ToArray();
for(int index=0;index<2;index++)
{
string side=index==0?"L":"R";float sign=index==0?1:-1;
var old=original[index];if(old.Corrected)continue;
var hip=old.Position;var knee=anatomy["LowerLeg."+side];
var leg=sections.Where(s=>s.Center.Y>knee.Y+(hip.Y-knee.Y)*.35f&&s.Center.Y<hip.Y+height*.01f&&
(s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.07f&&s.Radius<height*.1f).ToArray();
var top=leg.MaxBy(s=>s.Center.Y);
if(top is null||top.Center.Y>hip.Y-height*.025f||top.MinimumRadius<height*.012f)continue;
// The final contour is distorted by the groin. Fit the shaft below
// that transition, rather than extending the pinched contour center.
var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.02f&&s.Center.Y>top.Center.Y-height*.08f).ToArray();
if(shaft.Length<4)continue;
var mean=Geometry.Mean(shaft.Select(s=>s.Center));
float variance=shaft.Sum(s=>MathF.Pow(s.Center.Y-mean.Y,2));if(variance<height*height*1e-8f)continue;
var slope=shaft.Aggregate(Vector3.Zero,(value,s)=>value+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;
// A local inscribed radius locates the socket above the last
// separated leg section, independently of total body proportions.
float y=top.Center.Y+top.MinimumRadius;var candidate=mean+slope*(y-mean.Y);
if(!Geometry.Finite(candidate)||candidate.Y>=hip.Y||(candidate.X-center)*sign<height*.01f||
Vector3.Distance(candidate,hip)>Vector3.Distance(hip,knee)*.2f)continue;
if(!Enumerable.Range(0,21).All(i=>volume.Contains(Vector3.Lerp(knee,candidate,i/20f),height*.00001f)))continue;
candidates[index]=candidate;
}
// An isolated contour estimate cannot justify tilting the pelvis. Keep
// existing asymmetry, but reject a new height mismatch larger than the
// section sampling interval when the opposite socket lacks support.
if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(original[0].Position.Y-original[1].Position.Y)+height*.005f)return;
for(int i=0;i<2;i++)anatomy.Points[original[i].Role]=original[i] with{Position=candidates[i]};
}
}
Editor
library
namespace HumanoidRigger;
/// <summary>ModelDoc replaces namespace colons with underscores when importing bones.</summary>
public static class VmdlBoneNames
{
public static string Convert(string name)=>name.Replace(':','_');
public static void Validate(IEnumerable<RigBone> bones)
{
var names=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
foreach(var bone in bones)
{
string converted=Convert(bone.Name);
if(names.TryGetValue(converted,out var previous))
throw new InvalidOperationException($"Bone names '{previous}' and '{bone.Name}' both become '{converted}' in s&box. Rename one bone in the profile.");
names.Add(converted,bone.Name);
}
}
}
Editor
library
using System.IO.Compression;
using System.Net.Http;
using System.Security.Cryptography;
using System.Threading.Tasks;
namespace HumanoidRigger;
/// <summary>Pinned, hash-checked model/runtime cache. Downloads never contain user model data.</summary>
public static class HandModelAssets
{
public const string ModelHash="db0898ae717b76b075d9bf563af315b29562e11f8df5027a1ef07b02bef6d81c";
public const string RuntimeHash="dec964ab1ee36cc9b0ae247d13b376627992fc57dec0454354017ab8fd84f1ea";
public static string CacheDirectory=>Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),"sbox-humanoid-rigger","inference","hand-v1");
public static async Task<NativeHandModel> Load(string folder)
{
Directory.CreateDirectory(folder);
string model=Path.Combine(folder,"hand.onnx"),runtime=Path.Combine(folder,"onnxruntime.dll");
using var http=new HttpClient{Timeout=TimeSpan.FromSeconds(90)};
await Download(http,"https://media.githubusercontent.com/media/opencv/opencv_zoo/25f423d0e04c31a17254620e58febd7386da523b/models/handpose_estimation_mediapipe/handpose_estimation_mediapipe_2023feb.onnx",model,ModelHash);
if(!Valid(runtime,RuntimeHash))
{
string archive=Path.Combine(folder,"onnxruntime-1.23.2.zip");
await Download(http,"https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-win-x64-1.23.2.zip",archive,"0b38df9af21834e41e73d602d90db5cb06dbd1ca618948b8f1d66d607ac9f3cd");
using var zip=ZipFile.OpenRead(archive);
foreach(var name in new[]{"lib/onnxruntime.dll","LICENSE","ThirdPartyNotices.txt"})
{
var entry=zip.GetEntry("onnxruntime-win-x64-1.23.2/"+name)??throw new InvalidDataException("Missing runtime asset.");
string destination=Path.Combine(folder,Path.GetFileName(name)),temporary=destination+"."+Guid.NewGuid().ToString("N")+".tmp";
try{entry.ExtractToFile(temporary);if(name.EndsWith(".dll")&&!Valid(temporary,RuntimeHash))throw new InvalidDataException("Runtime checksum mismatch.");File.Move(temporary,destination,true);}
finally{if(File.Exists(temporary))File.Delete(temporary);}
}
}
if(!Valid(runtime,RuntimeHash)||!Valid(model,ModelHash))throw new InvalidDataException("Hand inference asset checksum mismatch.");
return new NativeHandModel(runtime,model);
}
public static bool Valid(string path,string hash)
{
if(!File.Exists(path))return false;
using var stream=File.OpenRead(path);return Convert.ToHexString(SHA256.HashData(stream)).Equals(hash,StringComparison.OrdinalIgnoreCase);
}
static async Task Download(HttpClient http,string url,string path,string hash)
{
if(Valid(path,hash))return;
string temporary=path+"."+Guid.NewGuid().ToString("N")+".tmp";
try
{
using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead);response.EnsureSuccessStatusCode();
using(var destination=File.Create(temporary))await response.Content.CopyToAsync(destination);
if(!Valid(temporary,hash))throw new InvalidDataException("Hand inference download checksum mismatch.");
File.Move(temporary,path,true);
}
finally{if(File.Exists(temporary))File.Delete(temporary);}
}
}
Editor
library
#nullable enable annotations
namespace HumanoidRigger.Formats.Fbx;
/// <summary>
/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of
/// typed properties, and nested child nodes.
///
/// Property values are stored as the closest CLR type to what the file contained:
/// <list type="bullet">
/// <item><c>short</c> ('Y'), <c>bool</c> ('C'), <c>int</c> ('I'), <c>float</c> ('F'),
/// <c>double</c> ('D'), <c>long</c> ('L')</item>
/// <item><c>float[]</c> ('f'), <c>double[]</c> ('d'), <c>long[]</c> ('l'),
/// <c>int[]</c> ('i'), <c>bool[]</c>-as-<c>byte[]</c> ('b')</item>
/// <item><c>string</c> ('S' — kept raw, may contain the <c>\x00\x01</c> name/class
/// separator; see <see cref="SplitName"/>), <c>byte[]</c> ('R')</item>
/// </list>
/// ASCII files store numbers only as <c>long</c> / <c>double</c> (and arrays as
/// <c>long[]</c> / <c>double[]</c>), so the typed accessors below convert tolerantly.
/// </summary>
public sealed class FbxNode
{
public string Name { get; }
public List<object> Properties { get; } = new();
public List<FbxNode> Children { get; } = new();
public FbxNode(string name) => Name = name;
/// <summary>First child with the given name, or null.</summary>
public FbxNode? Child(string name)
{
foreach (var c in Children)
if (c.Name == name)
return c;
return null;
}
/// <summary>All children with the given name, in document order.</summary>
public IEnumerable<FbxNode> ChildrenNamed(string name)
{
foreach (var c in Children)
if (c.Name == name)
yield return c;
}
/// <summary>
/// Property <paramref name="i"/> converted to <typeparamref name="T"/>.
/// Numeric scalars convert tolerantly across widths (e.g. an 'I' i32 read as long);
/// anything else must match the stored type exactly.
/// </summary>
public T Prop<T>(int i)
{
object v = RawProp(i);
if (v is T t)
return t;
var target = typeof(T);
// s&box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.
if (v is IConvertible && (ConvertTargets.Contains(target) || target == typeof(string)))
{
try
{
return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);
}
// ArithmeticException covers OverflowException, which is not s&box-whitelisted
catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)
{
throw new FormatException(
$"FBX node '{Name}': property {i} is {v.GetType().Name}, not convertible to {target.Name}.", ex);
}
}
throw new FormatException(
$"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {target.Name}.");
}
/// <summary>Property <paramref name="i"/> as a double array (converts f/l/i/b arrays).</summary>
public double[] AsDoubleArray(int i) => RawProp(i) switch
{
double[] d => d,
float[] f => Array.ConvertAll(f, x => (double)x),
long[] l => Array.ConvertAll(l, x => (double)x),
int[] n => Array.ConvertAll(n, x => (double)x),
byte[] b => Array.ConvertAll(b, x => (double)x),
bool[] o => Array.ConvertAll(o, x => x ? 1.0 : 0.0),
var v => throw TypeError(i, v, "double[]"),
};
/// <summary>Property <paramref name="i"/> as a float array (converts d/l/i/b arrays).</summary>
public float[] AsFloatArray(int i) => RawProp(i) switch
{
float[] f => f,
double[] d => Array.ConvertAll(d, x => (float)x),
long[] l => Array.ConvertAll(l, x => (float)x),
int[] n => Array.ConvertAll(n, x => (float)x),
byte[] b => Array.ConvertAll(b, x => (float)x),
bool[] o => Array.ConvertAll(o, x => x ? 1f : 0f),
var v => throw TypeError(i, v, "float[]"),
};
/// <summary>Property <paramref name="i"/> as a long array (converts i/b; d/f if integral).</summary>
public long[] AsLongArray(int i) => RawProp(i) switch
{
long[] l => l,
int[] n => Array.ConvertAll(n, x => (long)x),
byte[] b => Array.ConvertAll(b, x => (long)x),
bool[] o => Array.ConvertAll(o, x => x ? 1L : 0L),
double[] d => Array.ConvertAll(d, x => checked((long)x)),
float[] f => Array.ConvertAll(f, x => checked((long)x)),
var v => throw TypeError(i, v, "long[]"),
};
/// <summary>Property <paramref name="i"/> as an int array (converts b; l/d/f narrowing-checked).</summary>
public int[] AsIntArray(int i) => RawProp(i) switch
{
int[] n => n,
long[] l => Array.ConvertAll(l, x => checked((int)x)),
byte[] b => Array.ConvertAll(b, x => (int)x),
bool[] o => Array.ConvertAll(o, x => x ? 1 : 0),
double[] d => Array.ConvertAll(d, x => checked((int)x)),
float[] f => Array.ConvertAll(f, x => checked((int)x)),
var v => throw TypeError(i, v, "int[]"),
};
/// <summary>Property <paramref name="i"/> as raw bytes ('R' blobs or 'b' bool arrays).</summary>
public byte[] AsByteArray(int i) => RawProp(i) switch
{
byte[] b => b,
var v => throw TypeError(i, v, "byte[]"),
};
/// <summary>Property <paramref name="i"/> as a string (raw 'S' content, separators intact).</summary>
public string AsString(int i) => RawProp(i) switch
{
string s => s,
var v => throw TypeError(i, v, "string"),
};
/// <summary>
/// Splits an FBX object name into (name, class).
/// Binary files store <c>"Name\x00\x01Class"</c> (e.g. <c>"mixamorig:Hips\x00\x01Model"</c>);
/// ASCII files store <c>"Class::Name"</c> (e.g. <c>"Model::pelvis"</c>).
/// A plain string with neither separator yields (name, "").
/// </summary>
public static (string Name, string Class) SplitName(string raw)
{
int bin = raw.IndexOf("\0\x01", StringComparison.Ordinal);
if (bin >= 0)
return (raw[..bin], raw[(bin + 2)..]);
int ascii = raw.IndexOf("::", StringComparison.Ordinal);
if (ascii >= 0)
return (raw[(ascii + 2)..], raw[..ascii]);
return (raw, "");
}
/// <summary>Primitive scalar types <see cref="Prop{T}"/> converts to (whitelist-safe IsPrimitive substitute).</summary>
private static readonly HashSet<Type> ConvertTargets = new()
{
typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),
typeof(int), typeof(uint), typeof(long), typeof(ulong),
typeof(float), typeof(double), typeof(char),
};
private object RawProp(int i)
{
if (i < 0 || i >= Properties.Count)
throw new FormatException(
$"FBX node '{Name}': property index {i} out of range (has {Properties.Count}).");
return Properties[i];
}
private FormatException TypeError(int i, object v, string wanted) =>
new($"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {wanted}.");
}
Editor
library
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Regularize a limb's blend with its parent using the measured joint
/// thickness. Trials retain connectivity and are accepted after complete stress
/// testing and local repair; reviewed bones and source geometry never move.</summary>
internal static class JointWeightRepair
{
internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry? geometry=null)
{
var roles=rig.Bones.Select(b=>b.Role).ToHashSet();
var specifications=(geometry?.Poses??Deformation.Poses).Where(p=>Deformation.IsApplicable(p,roles)).ToArray();
var expected=specifications.Select(p=>p.Name).Order().ToArray();
if(!WeightRepair.HasCompleteEvidence(rig.Report,expected)||rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return rig;
geometry??=new ValidationGeometry(character);var faces=geometry.Faces;
var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
float height=character.AnatomicalHeight;
for(int pass=0;pass<4;pass++)
{
int before=rig.Report.StressTests.Sum(p=>p.ReversedTriangles);
foreach(var scheduled in rig.Report.StressTests.Where(p=>p.ReversedTriangles>0)
.OrderByDescending(p=>p.MaximumStretch>4||p.MinimumAreaRatio<.025f).ThenByDescending(p=>p.ReversedAreaFraction).ToArray())
{
var stress=rig.Report.StressTests.Single(p=>p.Pose==scheduled.Pose);
if(stress.ReversedTriangles==0)continue;
var specification=specifications.Single(p=>p.Name==stress.Pose);
if(geometry.Poses.Count==Deformation.Poses.Count&&!new[]{"UpperArm.","LowerArm.","UpperLeg.","LowerLeg."}.Any(specification.Role.StartsWith))continue;
int joint=Array.FindIndex(rig.Bones,b=>b.Role==specification.Role);var bone=rig.Bones[joint];
var child=rig.Bones.FirstOrDefault(b=>b.Parent==joint&&b.Deform);
if(child is null||bone.Parent<0||!rig.Bones[bone.Parent].Deform||Vector3.DistanceSquared(child.Position,bone.Position)<1e-8f)continue;
var axis=Vector3.Normalize(child.Position-bone.Position);var moving=new bool[rig.Bones.Length];moving[joint]=true;
for(int i=joint+1;i<moving.Length;i++)moving[i]=rig.Bones[i].Parent>=0&&moving[rig.Bones[i].Parent];
float sum=0;int count=0;
foreach(var mesh in character.Meshes)foreach(var p in mesh.Vertices)
{
var delta=p-bone.Position;float along=Vector3.Dot(delta,axis),radial=(delta-axis*along).Length();
if(Math.Abs(along)<height*.006f&&radial<height*.07f){sum+=radial;count++;}
}
float radius=Math.Clamp(count>0?sum/count:height*.025f,height*.01f,height*.065f);
var totals=rig.Weights.Select(part=>part.Select(weights=>MovingTotal(weights,moving)).ToArray()).ToArray();
var axial=character.Meshes.Select(mesh=>mesh.Vertices.Select(point=>Vector3.Dot(point-bone.Position,axis)).ToArray()).ToArray();
var trials=new List<(GeneratedRig Rig,StressResult Stress)>();
var blends=stress.ReversedTriangles<16?new[]{-.025f,-.05f,-.1f,-.2f,-.35f,.025f,.05f,.1f,.2f,.35f}:new[]{.5f,1f};
foreach(float width in new[]{2f,3f,4f,6f})foreach(float blend in blends)
{
var weights=character.Meshes.Select((mesh,part)=>mesh.Vertices.Select((point,vertex)=>
Blend(rig.Weights[part][vertex],rig.Profile.MaximumInfluences,rig.Bones.Length,moving,bone.Parent,
totals[part][vertex],axial[part][vertex]/(2*width*radius),blend)).ToArray()).ToArray();
var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Weights=weights,Anatomy=rig.Anatomy};
var result=RigValidator.MeasurePose(character,candidate,specification,faces,buffer,height);
if(result.ReversedTriangles>=stress.ReversedTriangles||result.ReversedAreaFraction>stress.ReversedAreaFraction)continue;
trials.Add((candidate,result));
// Only the best three trials are tested below. Release other
// dense weight buffers immediately, preserving stable tie order.
if(trials.Count>3)trials=trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3).ToList();
}
// A promising broad correction can expose a local seam. Run the
// normal cleanup and repair before deciding whether it is better.
foreach(var trial in trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3))
{
var report=RigValidator.ValidateAndRepair(character,trial.Rig,geometry);
if(!SkeletonSolver.BetterSkinning(report,rig.Report,expected))continue;
report.Repairs+=rig.Report.Repairs;report.RepairPasses+=rig.Report.RepairPasses+1;
trial.Rig.Report=report;rig=trial.Rig;break;
}
}
if(before==rig.Report.StressTests.Sum(p=>p.ReversedTriangles))break;
}
return rig;
}
static float MovingTotal(Influence[] weights,bool[] moving)
{
double total=0;foreach(var w in weights)if(moving[w.Bone])total+=w.Weight;
return(float)total;
}
static Influence[] Blend(Influence[] source,int maximum,int boneCount,bool[] moving,int parent,float total,float axial,float amount)
{
if(total<.0001f)return source;
float t=Math.Clamp(.5f+axial,0,1),envelope=t*t*(3-2*t);
if(amount<0)
{
if(total>.9999f)return source;
float target=total+(-amount)*total*(1-total)*(1-envelope);
var scaled=new Influence[source.Length];
for(int i=0;i<source.Length;i++){var w=source[i];scaled[i]=w with{Weight=w.Weight*(moving[w.Bone]?target/total:(1-target)/(1-total))};}
return Skinning.Cleanup(scaled,boneCount,maximum);
}
float retained=1-amount+envelope*amount;
var blended=new Influence[source.Length+1];
for(int i=0;i<source.Length;i++){var w=source[i];blended[i]=w with{Weight=w.Weight*(moving[w.Bone]?retained:1)};}
blended[^1]=new(parent,total*(1-retained));
return Skinning.Cleanup(blended,boneCount,maximum);
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Check every deforming joint in both directions on each axis.
/// Only faces reached by that joint's skin weights need geometric measurement.</summary>
internal static class JointCoverage
{
internal static (StressPose Pose,StressResult Result)[] Measure(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)
{
var result=new List<(StressPose,StressResult)>();
var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
for(int joint=0;joint<rig.Bones.Length;joint++)
{
if(!rig.Bones[joint].Deform)continue;
var moving=new bool[rig.Bones.Length];moving[joint]=true;
for(int b=joint+1;b<moving.Length;b++)moving[b]=rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];
var touched=rig.Weights.Select(p=>p.Select(w=>w.Any(i=>moving[i.Bone])).ToArray()).ToArray();
var faces=geometry.Faces.Select((p,m)=>p.Where(f=>touched[m][f.A]||touched[m][f.B]||touched[m][f.C]).ToArray()).ToArray();
foreach(var (axis,name) in new[]{(Vector3.UnitX,"X"),(Vector3.UnitY,"Y"),(Vector3.UnitZ,"Z")})foreach(float degrees in new[]{-30f,30f})
{
var pose=new StressPose($"Joint {rig.Bones[joint].Role} {name} {degrees:+0;-0}",rig.Bones[joint].Role,axis,degrees);
result.Add((pose,RigValidator.MeasurePose(character,rig,pose,faces,buffer,geometry.Height)));
}
}
return result.ToArray();
}
static bool Bad(StressResult result)=>result.ReversedTriangles>0||result.NonFiniteVertices>0||result.NonFiniteMeasurements>0||result.MaximumStretch>4||result.MinimumAreaRatio<.025f;
static double Score(IEnumerable<StressResult> results)=>results.Sum(r=>r.ReversedTriangles+1000000d*(r.NonFiniteVertices+r.NonFiniteMeasurements)+100*Math.Max(0,r.MaximumStretch/4-1)+100*Math.Max(0,1-r.MinimumAreaRatio/.025f));
internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig)
{
if(!rig.Report.Passed)return rig;
var geometry=new ValidationGeometry(character);var checks=Measure(character,rig,geometry);
if(checks.Any(c=>Bad(c.Result)))
{
var repaired=PoseWeightRepair.Improve(character,rig,geometry,checks);
if(!ReferenceEquals(repaired,rig)){rig=repaired;checks=Measure(character,rig,geometry);}
}
var trunk=new TrunkRegion(character,rig);
var normalHeat=new Lazy<Influence[][][]>(()=>HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:trunk).First());
var heat=new Lazy<Influence[][][]>(()=>HeatSkinning.Solve(character,rig));
var constraints=new Dictionary<string,StressPose>();
for(int pass=0;pass<4&&checks.Any(c=>Bad(c.Result));pass++)
{
var failed=checks.Where(c=>Bad(c.Result)).OrderByDescending(c=>c.Result.ReversedTriangles).Take(24).Select(c=>c.Pose).ToArray();
// Retain previously discovered failures after they are repaired.
// Otherwise a spine correction can undo the adjacent chest repair.
foreach(var pose in failed)constraints.TryAdd(pose.Name,pose);
var scope=new ValidationGeometry(character,Deformation.Poses.Concat(constraints.Values).ToArray(),trunk);
var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray()};
candidate.Report=RigValidator.ValidateAndRepair(character,candidate,scope);
candidate=JointWeightRepair.Improve(character,candidate,scope);
if(candidate.Report.StressTests.Any(p=>p.ReversedTriangles>0))
RefineSources(character,candidate,scope,failed,normalHeat,heat);
if(!candidate.Report.Passed||trunk.HasBleeding(character,candidate))break;
var standard=RigValidator.Validate(character,candidate);
if(!standard.Passed||standard.StressTests.Zip(rig.Report.StressTests).Any(p=>p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))break;
var next=Measure(character,candidate,geometry);
bool discovered=false;
foreach(var check in next.Where(c=>Bad(c.Result)))discovered|=constraints.TryAdd(check.Pose.Name,check.Pose);
if(Score(next.Select(c=>c.Result))>=Score(checks.Select(c=>c.Result)))
{if(discovered)continue;break;}
standard.Repairs=rig.Report.Repairs+candidate.Report.Repairs;standard.RepairPasses=rig.Report.RepairPasses+candidate.Report.RepairPasses+1;
candidate.Report=standard;rig=candidate;checks=next;
}
rig.Report.JointStressTests.AddRange(checks.Select(c=>c.Result));
var remaining=checks.Where(c=>Bad(c.Result)).ToArray();
if(remaining.Length>0)rig.Report.Issues.Add(new("joint-deformation",$"{remaining.Length} joint motions still have unsafe deformation: {string.Join(", ",remaining.Select(c=>c.Pose.Role).Distinct())}.",true));
return rig;
}
static void RefineSources(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,StressPose[] failed,params Lazy<Influence[][][]>[] fields)
{
var roots=failed.Select(p=>p.Role).ToHashSet();var moving=new bool[rig.Bones.Length];
for(int b=0;b<moving.Length;b++)moving[b]=roots.Contains(rig.Bones[b].Role)||rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];
foreach(var field in fields)
{
Influence[][][] source;
try{source=field.Value;}catch(InvalidOperationException){continue;}
foreach(float amount in new[]{1f,.5f,.25f})
{
var proposed=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();
for(int p=0;p<proposed.Length;p++)for(int v=0;v<proposed[p].Length;v++)
{
float support=Math.Min(1,rig.Weights[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight)+source[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight))*amount;
if(support<.001f)continue;
var weights=Skinning.Cleanup(rig.Weights[p][v].Select(w=>w with{Weight=w.Weight*(1-support)})
.Concat(source[p][v].Select(w=>w with{Weight=w.Weight*support})),rig.Bones.Length,rig.Profile.MaximumInfluences);
if(geometry.Trunk?.Allows(p,v,character.Meshes[p].Vertices[v],weights)==false)continue;
proposed[p][v]=weights;
}
rig.Report=SurfaceRepair.TryWeights(character,rig,rig.Report,proposed,geometry);
rig.Report=SurfaceRepair.Improve(character,rig,rig.Report,geometry);
if(rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return;
}
}
}
}
Editor
library
global using System; global using System.Collections.Generic; global using System.Linq; global using System.IO;
Editor
library
// Copied from humanoid-retargeter Editor/HumanoidRetargeter/EditorPipeline.cs.
using Editor;
using Sandbox;
namespace HumanoidRigger.Editor;
internal static partial class MaterialAssets
{
static readonly string[] TextureExtensions =
{ ".png", ".jpg", ".jpeg", ".tga", ".dds", ".webp", ".vmat", ".vtex" };
/// <summary>Copies texture sidecars of a picked target model into the output folder:
/// loose image files next to it, and a "textures" folder next to it or next to its
/// parent (the source/-plus-textures/ layout). Per-file best effort - a failed texture
/// must never fail the conversion.</summary>
static void CopySidecarTextures( string sourceDir, string destDir )
{
try
{
if ( sourceDir is null || destDir is null )
return;
sourceDir = Path.GetFullPath( sourceDir );
destDir = Path.GetFullPath( destDir );
if ( string.Equals( sourceDir, destDir, StringComparison.OrdinalIgnoreCase ) )
return;
// Every copied file must be REGISTERED: assets copied onto disk mid-session are
// unknown to the asset system, so the material chain cannot generate their vtex
// resources - the renderer then logs "Texture manager doesn't know about
// texture ...generated.vtex" MANY TIMES PER FRAME, which is both the
// purple/black flicker and a preview running at ~2 fps (user report).
foreach ( var file in Directory.GetFiles( sourceDir ) )
{
if ( !TextureExtensions.Contains( Path.GetExtension( file ).ToLowerInvariant() ) )
continue;
var destFile = Path.Combine( destDir, Path.GetFileName( file ) );
Try( () => { File.Copy( file, destFile, true ); return true; } );
Try( () => AssetSystem.RegisterFile( destFile ) );
}
foreach ( var candidate in new[]
{
Path.Combine( sourceDir, "textures" ),
Path.Combine( Path.GetDirectoryName( sourceDir ) ?? sourceDir, "textures" ),
} )
{
if ( !Directory.Exists( candidate ) )
continue;
var destTextures = Path.Combine( destDir, "textures" );
Directory.CreateDirectory( destTextures );
foreach ( var file in Directory.GetFiles( candidate, "*", SearchOption.AllDirectories ) )
{
var relative = Path.GetRelativePath( candidate, file );
var destFile = Path.Combine( destTextures, relative );
Try( () =>
{
Directory.CreateDirectory( Path.GetDirectoryName( destFile ) );
File.Copy( file, destFile, true );
return true;
} );
Try( () => AssetSystem.RegisterFile( destFile ) );
}
break; // first existing candidate wins
}
}
catch ( Exception e )
{
Log.Warning( $"[sbox-humanoid-rigger] sidecar texture copy failed: {e.Message}" );
}
}
static T Try<T>(Func<T> action){try{return action();}catch(Exception e){Log.Warning(e.Message);return default;}}
}
Editor
library
using Editor;
using Sandbox;
namespace HumanoidRigger.Editor;
/// <summary>Copied from Humanoid Retargeter's RetargetWindow.Chip.
/// Keep its dimensions, typography, fill and radius aligned with the suite.</summary>
sealed class StatusChip : Widget
{
readonly string text;
readonly Color color;
public StatusChip(Widget parent,string text,Color color) : base(parent)
{
this.text=text;
this.color=color;
FixedHeight=20;
FixedWidth=7.2f*text.Length+18;
}
protected override void OnPaint()
{
Paint.ClearPen();
Paint.SetBrush(color.WithAlpha(.18f));
Paint.DrawRect(LocalRect,LocalRect.Height*.5f);
Paint.SetPen(color);
Paint.SetDefaultFont(7,600);
Paint.DrawText(LocalRect,text);
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Resolve up from body geometry, then an unambiguous quarter-turn
/// from separated lower legs. Feet select the forward sign.</summary>
internal static class HumanoidFacing
{
public static ImportedCharacter Normalize(ImportedCharacter character)
{
var up=HumanoidUp.Find(character);
if(up!=Vector3.UnitY)
{
Vector3 Upright(Vector3 p)=>up==Vector3.UnitX?new(-p.Y,p.X,p.Z)
:up==-Vector3.UnitX?new(p.Y,-p.X,p.Z)
:up==-Vector3.UnitY?new(p.X,-p.Y,-p.Z)
:up==Vector3.UnitZ?new(p.X,p.Z,-p.Y):new(p.X,-p.Z,p.Y);
character=Reorient(character,Upright,"The character's up direction was corrected from its body geometry.");
}
var body=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
if(body.Length<100)return character;
float bottom=body.Min(p=>p.Y),height=body.Max(p=>p.Y)-bottom;
var legs=body.Where(p=>p.Y>bottom+height*.15f&&p.Y<bottom+height*.35f).Distinct().ToArray();
if(legs.Length<20)return character;
var center=Geometry.Mean(legs);
float xVariance=legs.Average(p=>(p.X-center.X)*(p.X-center.X));
float zVariance=legs.Average(p=>(p.Z-center.Z)*(p.Z-center.Z));
if(zVariance<xVariance*3)return character;
float middle=(BodyDetector.Quantile(legs.Select(p=>p.Z),.1f)+BodyDetector.Quantile(legs.Select(p=>p.Z),.9f))*.5f;
if(legs.Count(p=>Math.Abs(p.Z-middle)<height*.012f)>legs.Length*.15f)return character;
var feet=body.Where(p=>p.Y<bottom+height*.07f).ToArray();
if(feet.Length<8)return character;
float forward=(BodyDetector.Quantile(feet.Select(p=>p.X),.1f)+BodyDetector.Quantile(feet.Select(p=>p.X),.9f))*.5f-center.X;
forward=SeparatedFeetForward(character,bottom,height,middle)??forward;
if(Math.Abs(forward)<height*.012f)return character;
float sign=forward<0?1:-1;
Vector3 Turn(Vector3 p)=>new(sign*p.Z,p.Y,-sign*p.X);
return Reorient(character,Turn,"The character was turned to face forward.");
}
static float? SeparatedFeetForward(ImportedCharacter character,float bottom,float height,float middle)
{
// Long hands can reach the floor. If the lower slice contains more than
// two limbs, follow the inner calf surfaces down to their own feet.
var origin=(character.Minimum+character.Maximum)*.5f;origin.Y=bottom+height*.25f;
var sections=MeshSections.Cut(character,origin,Vector3.UnitY,(character.Maximum-character.Minimum).Length(),height*1e-5f);
if(sections.Length<=2)return null;
var left=sections.Where(s=>s.Center.Z>middle+height*.025f).OrderBy(s=>s.Center.Z).FirstOrDefault();
var right=sections.Where(s=>s.Center.Z<middle-height*.025f).OrderByDescending(s=>s.Center.Z).FirstOrDefault();
if(left is null||right is null)return null;
var mesh=Geometry.Merge(character.Meshes.Where(m=>m.Kind==MeshKind.Body));
var neighbors=Geometry.Neighbors(mesh,height*1e-5f);var visited=new bool[mesh.Vertices.Length];var queue=new Queue<int>();
foreach(var section in new[]{left,right})
{
int seed=-1;float distance=float.PositiveInfinity;
for(int i=0;i<mesh.Vertices.Length;i++)
{
float candidate=Vector3.DistanceSquared(mesh.Vertices[i],section.Center);
if(candidate<distance){seed=i;distance=candidate;}
}
if(seed<0||distance>height*height*.08f*.08f)return null;
if(!visited[seed]){visited[seed]=true;queue.Enqueue(seed);}
}
while(queue.TryDequeue(out int vertex))foreach(int next in neighbors[vertex])
if(!visited[next]&&mesh.Vertices[next].Y<bottom+height*.4f){visited[next]=true;queue.Enqueue(next);}
var feet=mesh.Vertices.Where((p,i)=>visited[i]&&p.Y<bottom+height*.07f).Select(p=>p.X).ToArray();
if(feet.Length<8)return null;
return(BodyDetector.Quantile(feet,.1f)+BodyDetector.Quantile(feet,.9f))*.5f-(left.Center.X+right.Center.X)*.5f;
}
static ImportedCharacter Reorient(ImportedCharacter character,Func<Vector3,Vector3> turn,string warning)
{
return new ImportedCharacter{Name=character.Name,SourcePath=character.SourcePath,SourceUnitCm=character.SourceUnitCm,SourceUpAxis=character.SourceUpAxis,
HasExistingSkin=character.HasExistingSkin,ExistingBones=character.ExistingBones.Select(b=>b with{Position=turn(b.Position)}).ToArray(),
Meshes=character.Meshes.Select(m=>m with{Vertices=m.Vertices.Select(turn).ToArray(),CornerNormals=m.CornerNormals.Select(turn).ToArray()}).ToArray(),
Materials=character.Materials,EmbeddedTextures=character.EmbeddedTextures,
ImportWarnings=character.ImportWarnings.Append(warning).ToArray()};
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Check declared up against a head, torso and paired lower-limb
/// cross sections. Ambiguous or open geometry retains the imported axes.</summary>
internal static class HumanoidUp
{
internal static Vector3 Find(ImportedCharacter character)
{
var points=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
if(points.Length<100)return Vector3.UnitY;
var minimum=points.Aggregate(Vector3.Min);var maximum=points.Aggregate(Vector3.Max);
var center=(minimum+maximum)*.5f;float reach=(maximum-minimum).Length();
bool Supports(Vector3 up)
{
float bottom=points.Min(p=>Vector3.Dot(p,up)),height=points.Max(p=>Vector3.Dot(p,up))-bottom;
if(height<.001f)return false;
MeshSections.Section[] Sections(float fraction)
{
var origin=center+up*(bottom+height*fraction-Vector3.Dot(center,up));
return MeshSections.Cut(character,origin,up,reach,height*1e-5f)
.Where(s=>s.Area>height*height*1e-6f).OrderByDescending(s=>s.Area).ToArray();
}
var heads=Sections(.92f);
if(heads.Length==0||heads[0].Area<heads.Sum(s=>s.Area)*.7f||heads[0].Radius>height*.3f)return false;
var torsos=Sections(.60f);if(torsos.Length==0)return false;
var head=heads[0].Center;var torso=torsos[0].Center;
Vector3 Horizontal(Vector3 p)=>p-up*Vector3.Dot(p,up);
if(Horizontal(head-torso).Length()>height*.22f)return false;
Vector3? previous=null;int evidence=0;
foreach(float fraction in new[]{.25f,.35f})
{
var limbs=Sections(fraction);float best=float.PositiveInfinity;Vector3 direction=default;
for(int i=0;i<limbs.Length;i++)for(int j=i+1;j<limbs.Length;j++)
{
var a=limbs[i];var b=limbs[j];var delta=b.Center-a.Center;float length=delta.Length();
if(length<height*.06f||length>height*.45f||Math.Min(a.Area,b.Area)<Math.Max(a.Area,b.Area)*.2f)continue;
if(length<(a.Radius+b.Radius)*1.1f)continue;
float offset=Horizontal((a.Center+b.Center)*.5f-torso).Length();
if(offset>height*.2f||offset>=best)continue;
direction=delta/length;best=offset;
}
if(!float.IsFinite(best))continue;
if(previous is {} prior&&Math.Abs(Vector3.Dot(prior,direction))<.85f)return false;
previous=direction;evidence++;
}
return evidence>0;
}
// A plausible declared up always wins. Correct only a unique supported
// alternative; the longest model dimension alone may simply be its arms.
if(Supports(Vector3.UnitY))return Vector3.UnitY;
Vector3? candidate=null;
foreach(var up in new[]{Vector3.UnitX,-Vector3.UnitX,-Vector3.UnitY,Vector3.UnitZ,-Vector3.UnitZ})
{
if(!Supports(up))continue;
if(candidate is not null)return Vector3.UnitY;
candidate=up;
}
return candidate??Vector3.UnitY;
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
using Vector2=System.Numerics.Vector2;
/// <summary>Closed triangle-plane contours, with area centers independent of vertex density.</summary>
internal static class MeshSections
{
internal record Section(Vector3 Center,float Area,float Radius,float MinimumRadius);
internal static Section[] Cut(ImportedCharacter character,Vector3 origin,Vector3 normal,float reach,float tolerance)
{
normal=Vector3.Normalize(normal);var u=Vector3.Normalize(Vector3.Cross(normal,Math.Abs(normal.Z)>.9f?Vector3.UnitY:Vector3.UnitZ));var v=Vector3.Cross(normal,u);
var points=new List<Vector2>();var edges=new HashSet<(int,int)>();var cells=new Dictionary<(int,int),List<int>>();
int Node(Vector3 position)
{
var delta=position-origin;var p=new Vector2(Vector3.Dot(delta,u),Vector3.Dot(delta,v));
int x=(int)Math.Floor(p.X/tolerance),y=(int)Math.Floor(p.Y/tolerance);
for(int i=-1;i<=1;i++)for(int j=-1;j<=1;j++)if(cells.TryGetValue((x+i,y+j),out var nearby))
foreach(int n in nearby)if(Vector2.DistanceSquared(points[n],p)<=tolerance*tolerance)return n;
int index=points.Count;points.Add(p);if(!cells.TryGetValue((x,y),out var bucket))cells[(x,y)]=bucket=[];bucket.Add(index);return index;
}
var cut=new Vector3[3];
foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))for(int t=0;t<mesh.Triangles.Length;t+=3)
{
int count=0;
for(int e=0;e<3;e++)
{
var a=mesh.Vertices[mesh.Triangles[t+e]];var b=mesh.Vertices[mesh.Triangles[t+(e+1)%3]];
float da=Vector3.Dot(a-origin,normal),db=Vector3.Dot(b-origin,normal);
if((da<=0&&db>0)||(db<=0&&da>0))cut[count++]=Vector3.Lerp(a,b,da/(da-db));
}
if(count!=2||Vector3.Distance(cut[0],origin)>reach||Vector3.Distance(cut[1],origin)>reach)continue;
int first=Node(cut[0]),second=Node(cut[1]);if(first!=second)edges.Add((Math.Min(first,second),Math.Max(first,second)));
}
var neighbors=points.Select(_=>new List<int>()).ToArray();foreach(var(a,b)in edges){neighbors[a].Add(b);neighbors[b].Add(a);}
var sections=new List<Section>();var seen=new bool[points.Count];
for(int start=0;start<points.Count;start++)
{
if(seen[start])continue;var component=new List<int>();var queue=new Queue<int>();queue.Enqueue(start);seen[start]=true;
while(queue.TryDequeue(out int n)){component.Add(n);foreach(int next in neighbors[n])if(!seen[next]){seen[next]=true;queue.Enqueue(next);}}
if(component.Count<6||component.Any(n=>neighbors[n].Count!=2))continue;
var polygon=new List<Vector2>();int previous=-1,current=start;
do{polygon.Add(points[current]);int next=neighbors[current].First(n=>n!=previous);previous=current;current=next;}while(current!=start&&polygon.Count<=component.Count);
if(current!=start||polygon.Count!=component.Count)continue;
float twiceArea=0;var weighted=Vector2.Zero;
for(int i=0;i<polygon.Count;i++){var a=polygon[i];var b=polygon[(i+1)%polygon.Count];float cross=a.X*b.Y-b.X*a.Y;twiceArea+=cross;weighted+=(a+b)*cross;}
if(Math.Abs(twiceArea)<tolerance*tolerance)continue;
var center=weighted/(3*twiceArea);float radius=polygon.Max(p=>Vector2.Distance(center,p));
float minimum=float.PositiveInfinity;
for(int i=0;i<polygon.Count;i++)
{
var a=polygon[i];var b=polygon[(i+1)%polygon.Count];var ab=b-a;
float t=Math.Clamp(Vector2.Dot(center-a,ab)/Math.Max(ab.LengthSquared(),1e-12f),0,1);minimum=Math.Min(minimum,Vector2.Distance(center,a+ab*t));
}
sections.Add(new(origin+u*center.X+v*center.Y,Math.Abs(twiceArea)*.5f,radius,minimum));
}
return sections.ToArray();
}
}
Editor
library
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Centers non-thumb chains on their own closed mesh sections, preserving
/// supported joint spacing and repairing chains collapsed near a fingertip.</summary>
public static class FingerAlignment
{
public static int Refine(ImportedCharacter character,Anatomy anatomy,string side)
{
if(side is not ("L" or "R"))throw new ArgumentException("Unknown hand side.");
var fingers=new[]{"Index","Middle","Ring","Pinky"};
var chains=fingers.Select(f=>Enumerable.Range(1,3).Select(i=>f+i+"."+side).Append(f+"Tip."+side).ToArray())
.Where(roles=>roles.All(r=>anatomy.Points.TryGetValue(r,out var p)&&!p.Corrected&&p.Confidence>=.35f)).ToArray();
if(chains.Length==0)return 0;
var wrist=anatomy["Hand."+side];float h=anatomy.Height;
float reach=chains.SelectMany(r=>r).Max(r=>Vector3.Distance(wrist,anatomy[r]))+h*.035f;
var local=HandSurface(character,wrist,reach);
// Containment uses the original complete shells, never the cropped analysis surface.
var volume=new SurfaceVisibility(character.Meshes.Where(m=>m.Kind==MeshKind.Body),h*.00001f);
int changed=0;
foreach(var roles in chains)
{
var previous=roles.Select(r=>anatomy[r]).ToArray();var fitted=Fit(local,volume,wrist,previous,h);
if(fitted is null)continue;
for(int i=0;i<3;i++)anatomy.Points[roles[i]]=anatomy.Points[roles[i]] with{Position=fitted[i]};
changed++;
}
return changed;
}
static Vector3[]? Fit(ImportedCharacter local,SurfaceVisibility volume,Vector3 wrist,Vector3[] previous,float h)
{
var tip=previous[3];var inward=wrist-tip;if(inward.LengthSquared()<h*h*.000001f)return null;
var axis=Vector3.Normalize(inward);var center=tip;float step=h*.0015f;
var path=new List<Vector3>{tip};var radii=new List<float>();var areas=new List<float>();bool ended=false;
for(int i=0;i<75;i++)
{
var seed=center+axis*step;
MeshSections.Section? section=null;
// A plane through a mesh vertex can produce an ambiguous contour.
// Nearby parallel cuts recover it without changing or welding geometry.
foreach(float offset in new[]{0f,.15f,-.15f,.35f,-.35f})
{
section=MeshSections.Cut(local,seed+axis*(step*offset),axis,h*.035f,h*.00001f)
.Where(s=>s.Radius>h*.0007f&&s.Radius<h*.016f&&Vector3.Distance(s.Center,seed)<h*.018f)
.MinBy(s=>Vector3.DistanceSquared(s.Center,seed));
if(section is not null)break;
}
if(section is null){ended=true;break;}
var difference=section.Center-center;
if(path.Count>1&&difference.Length()>h*.006f){ended=true;break;}
if(path.Count>5&§ion.Area>areas.TakeLast(3).Average()*1.8f){ended=true;break;}
if(path.Count>2&&volume.Blocked(center,section.Center,h*.00001f)){ended=true;break;}
if(difference.LengthSquared()<1e-12f)return null;
var tangent=Vector3.Normalize(difference);
if(path.Count>1&&Vector3.Dot(axis,tangent)<.3f){ended=true;break;}
center=section.Center;path.Add(center);radii.Add(section.Radius);areas.Add(section.Area);
if(path.Count>2)axis=Vector3.Normalize(Vector3.Lerp(axis,tangent,.25f));
if(Vector3.Distance(center,tip)>inward.Length()*.8f)return null;
}
if(path.Count<8||!ended)return null;
var extension=center+axis*radii[^1]*.5f;
if(volume.Contains(extension,h*.00001f)&&!volume.Blocked(center,extension,h*.00001f))path.Add(extension);
path.Reverse();var distances=new float[path.Count];
for(int i=1;i<path.Count;i++)distances[i]=distances[i-1]+Vector3.Distance(path[i-1],path[i]);
float coverage=Vector3.Distance(path[0],tip)/Vector3.Distance(previous[0],tip);
if(coverage<.6f)return null;
// The webbing can end the trace before a well-supported palm knuckle.
// Keep that base and still center the distal joints on the recovered digit.
bool retainBase=coverage<.85f;
(Vector3 Point,float Offset) Project(Vector3 point)
{
float best=float.PositiveInfinity,offset=0;var result=point;
for(int i=1;i<path.Count;i++)
{
var closest=Geometry.ClosestOnSegment(point,path[i-1],path[i]);float distance=Vector3.DistanceSquared(point,closest);
if(distance>=best)continue;best=distance;result=closest;offset=distances[i-1]+Vector3.Distance(closest,path[i-1]);
}
return(result,offset);
}
// A centered chain does not need a new proportion-based fit.
if(previous.Skip(retainBase?1:0).Take(retainBase?2:3).Average(p=>Vector3.Distance(p,Project(p).Point))<h*.00025f)return null;
var fitted=new[]{0f,.5f,.78f,1f}.Select(f=>
{
float target=distances[^1]*f;int i=Array.FindIndex(distances,d=>d>=target);if(i<=0)return path[0];
return Vector3.Lerp(path[i-1],path[i],(target-distances[i-1])/Math.Max(distances[i]-distances[i-1],1e-8f));
}).ToArray();
if(retainBase)fitted[0]=previous[0];
if(Vector3.Distance(previous[0],tip)>=Vector3.Distance(fitted[0],tip)*.6f)
{
var second=Project(previous[1]);var third=Project(previous[2]);
if(second.Offset<h*.001f||third.Offset-second.Offset<h*.001f||distances[^1]-third.Offset<h*.001f)return null;
fitted[1]=second.Point;fitted[2]=third.Point;
}
for(int bone=0;bone<3;bone++)for(int sample=0;sample<9;sample++)
if(!volume.Contains(Vector3.Lerp(fitted[bone],fitted[bone+1],sample/9f),h*.00001f))return null;
return fitted;
}
static ImportedCharacter HandSurface(ImportedCharacter character,Vector3 wrist,float radius)
{
var parts=new List<MeshPart>();float squared=radius*radius;
foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))
{
var triangles=new List<int>();
for(int i=0;i<mesh.Triangles.Length;i+=3)
{
var a=mesh.Vertices[mesh.Triangles[i]];var b=mesh.Vertices[mesh.Triangles[i+1]];var c=mesh.Vertices[mesh.Triangles[i+2]];
var nearest=Vector3.Clamp(wrist,Vector3.Min(a,Vector3.Min(b,c)),Vector3.Max(a,Vector3.Max(b,c)));
if(Vector3.DistanceSquared(nearest,wrist)>squared)continue;
triangles.Add(mesh.Triangles[i]);triangles.Add(mesh.Triangles[i+1]);triangles.Add(mesh.Triangles[i+2]);
}
if(triangles.Count>0)parts.Add(new(mesh.Name,mesh.Vertices,triangles.ToArray(),MeshKind.Body));
}
return new ImportedCharacter{Meshes=parts.ToArray()};
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Angle-weighted outward normals for closed, consistently oriented
/// components. Uncertain/open surfaces contribute no directional prior.</summary>
internal static class SkinningNormals
{
internal static Vector3[] ClosedSurface(Vector3[] points,int[] triangles)
{
var mesh=new MeshPart("Skinning surface",points,triangles,MeshKind.Body);
var components=Geometry.Components(mesh);int count=components.Max()+1;
var origins=new Vector3[count];var assigned=new bool[count];
for(int v=0;v<points.Length;v++)if(!assigned[components[v]]){origins[components[v]]=points[v];assigned[components[v]]=true;}
var volumes=new double[count];var closed=Enumerable.Repeat(true,count).ToArray();
var normals=new Vector3[points.Length];var edges=new Dictionary<(int,int),(int Count,int Direction)>();
for(int t=0;t<triangles.Length;t+=3)
{
int a=triangles[t],b=triangles[t+1],c=triangles[t+2];if(a==b||a==c||b==c)continue;
var normal=Vector3.Cross(points[b]-points[a],points[c]-points[a]);float area=normal.Length();
if(area<1e-12f){closed[components[a]]=false;continue;}
normal/=area;
var origin=origins[components[a]];
volumes[components[a]]+=Vector3.Dot(points[a]-origin,Vector3.Cross(points[b]-origin,points[c]-origin));
for(int corner=0;corner<3;corner++)
{
int v=triangles[t+corner],n=triangles[t+(corner+1)%3],o=triangles[t+(corner+2)%3];
var x=points[n]-points[v];var y=points[o]-points[v];
normals[v]+=normal*MathF.Atan2(Vector3.Cross(x,y).Length(),Vector3.Dot(x,y));
var key=(Math.Min(v,n),Math.Max(v,n));var edge=edges.GetValueOrDefault(key);
edges[key]=(edge.Count+1,edge.Direction+(v<n?1:-1));
}
}
foreach(var edge in edges)if(edge.Value.Count!=2||edge.Value.Direction!=0)closed[components[edge.Key.Item1]]=false;
for(int v=0;v<normals.Length;v++)
{
int component=components[v];float length=normals[v].Length();
normals[v]=closed[component]&&Math.Abs(volumes[component])>1e-12&&length>1e-6f
?normals[v]*(Math.Sign(volumes[component])/length):Vector3.Zero;
}
return normals;
}
}
Editor
library
#nullable enable annotations
using System.Numerics;
namespace HumanoidRigger;
using Vector3 = System.Numerics.Vector3;
public sealed record ValidationIssue(string Code,string Message,bool Error);
public sealed record StressResult(string Pose,float MaximumStretch,float MinimumAreaRatio,int NonFiniteVertices,float SourceEdgeLength=0,float DeformedEdgeLength=0,int NonFiniteMeasurements=0,int ReversedTriangles=0,float ReversedAreaFraction=0);
public sealed class ValidationReport
{
public List<ValidationIssue> Issues {get;}=[];
public List<StressResult> StressTests {get;}=[];
public List<StressResult> JointStressTests {get;}=[];
public int Repairs {get;set;}
public int RepairPasses {get;set;}
public bool Passed=>Issues.All(i=>!i.Error) && StressTests.Count>0;
}
public static class RigValidator
{
public static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig)
=>ValidateAndRepair(character,rig,new ValidationGeometry(character));
internal static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)
{
int repairs=0;
foreach(var part in rig.Weights) for(int v=0;v<part.Length;v++)
{
var cleaned=Skinning.Cleanup(part[v],rig.Bones.Length,rig.Profile.MaximumInfluences);
if(!cleaned.SequenceEqual(part[v])) {part[v]=cleaned;repairs++;}
}
var report=Validate(character,rig,null,geometry);report.Repairs=repairs;
return SurfaceRepair.Improve(character,rig,WeightRepair.Improve(character,rig,report,geometry),geometry);
}
public static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig)
=>Validate(character,rig,null);
internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces)
=>Validate(character,rig,faces,null);
internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces,ValidationGeometry? geometry)
{
var height=geometry?.Height??character.AnatomicalHeight;var r=new ValidationReport();void Error(string code,string text)=>r.Issues.Add(new(code,text,true));
try{rig.Profile.Validate();}catch(Exception e){Error("profile",e.Message);return r;}
var roles=new HashSet<string>();var names=new HashSet<string>();
var body=geometry?.Body??character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();
for(int i=0;i<rig.Bones.Length;i++)
{
var b=rig.Bones[i];
if(!roles.Add(b.Role)||!names.Add(b.Name))Error("duplicate","Duplicate bone assignment.");
bool validParent=b.Parent<i&&b.Parent>=-1;
if(!validParent)Error("hierarchy","Invalid skeleton hierarchy.");
if(!Geometry.Finite(b.Position)||!float.IsFinite(b.Rotation.LengthSquared())||Math.Abs(b.Rotation.LengthSquared()-1)>.001f)Error("frame","Invalid bone orientation or position.");
if(b.Deform&&body.Length>0&&(geometry?.JointDistanceSquared(b.Position)??body.Min(p=>Vector3.DistanceSquared(p,b.Position)))>height*height*.15f*.15f)
Error("joint-placement",$"{b.Role} is too far from the character. Check its landmark.");
var definition=rig.Profile.Bones.FirstOrDefault(d=>d.Role==b.Role);
if(definition is null || definition.Name!=b.Name || !validParent || (b.Parent<0 ? null : rig.Bones[b.Parent].Role)!=definition.Parent)Error("mapping","Skeleton differs from the selected profile.");
}
foreach(var b in rig.Profile.Bones.Where(b=>b.Required))if(!roles.Contains(b.Role))Error("required",$"Missing {b.Role}.");
if(rig.Weights.Length!=character.Meshes.Length){Error("weights","Missing mesh skinning.");return r;}
for(int p=0;p<rig.Weights.Length;p++)
{
if(rig.Weights[p].Length!=character.Meshes[p].Vertices.Length){Error("weights","Skinning vertex count mismatch.");continue;}
foreach(var vertex in rig.Weights[p])
{
if(vertex.Length==0||vertex.Length>rig.Profile.MaximumInfluences)Error("influences","Invalid influence count.");
if(vertex.Any(i=>i.Bone<0||i.Bone>=rig.Bones.Length||!float.IsFinite(i.Weight)||i.Weight<=0))Error("influences","Invalid weight or bone index.");
if(Math.Abs(vertex.Sum(i=>i.Weight)-1)>.0001f)Error("normalization","Weights do not sum to one.");
}
}
if(r.Issues.Any(i=>i.Error))return r;
if(geometry?.Trunk?.HasBleeding(character,rig)==true){Error("weight-bleeding","A weight repair reintroduced a remote torso attachment.");return r;}
var ends=RigGeometry.SegmentEnds(rig);
var locality=new SkinningLocality(character,rig,ends);
for(int p=0;p<character.Meshes.Length;p++)
{
var mesh=character.Meshes[p];if(mesh.Kind==MeshKind.Accessory)continue;
bool remote=false;
for(int v=0;v<mesh.Vertices.Length&&!remote;v++)
foreach(var influence in rig.Weights[p][v])
{
if(!rig.Bones[influence.Bone].Deform){Error("nondeforming-influence","Skinning references a non-deforming bone.");remote=true;break;}
if(influence.Weight>.05f&&Vector3.Distance(mesh.Vertices[v],Geometry.ClosestOnSegment(mesh.Vertices[v],rig.Bones[influence.Bone].Position,ends[influence.Bone]))>locality.Limit(influence.Bone,mesh.Vertices[v]))
{Error("weight-region",$"Mesh '{mesh.Name}' is influenced by a distant anatomical region ({rig.Bones[influence.Bone].Role}).");remote=true;break;}
}
}
if(r.Issues.Any(i=>i.Error))return r;
faces??=geometry?.Faces??character.Meshes.Select(BindTriangle.Measure).ToArray();
var specifications=(geometry?.Poses??Deformation.Poses).ToArray();
var tests=new StressResult[specifications.Length];
Vector3[][] Buffers()=>character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();
void Measure(int i,Vector3[][] buffer)
{
if(Deformation.IsApplicable(specifications[i],roles))tests[i]=MeasurePose(character,rig,specifications[i],faces,buffer,height);
}
// Poses read the same frozen weights and write separate buffers. Keep
// report order and each pose's arithmetic serial and deterministic.
int workers=RigWork.WorkerCount(character.Meshes.Sum(m=>m.Vertices.Length));
RigWork.For(specifications.Length,workers,Buffers,Measure);
for(int i=0;i<specifications.Length;i++)
{
var pose=specifications[i];var test=tests[i];
if(test is null){r.Issues.Add(new("optional-pose",$"{pose.Name}: optional joints absent.",false));continue;}
r.StressTests.Add(test);
if(test.ReversedTriangles>0)r.Issues.Add(new("surface-reversal",$"{pose.Name}: {test.ReversedTriangles} surface triangles reverse orientation.",false));
if(test.NonFiniteVertices>0||test.NonFiniteMeasurements>0)Error("deformation",$"{pose.Name}: deformation produced non-finite coordinates or measurements.");
else if(test.MaximumStretch>4||test.MinimumAreaRatio<.025f)Error("deformation",$"{pose.Name}: unsafe deformation (stretch {test.MaximumStretch:F2}, area ratio {test.MinimumAreaRatio:F3}).");
}
return r;
}
internal static StressResult MeasurePose(ImportedCharacter character,GeneratedRig rig,StressPose pose,BindTriangle[][] faces,Vector3[][] deformed,float height)
{
var transforms=Deformation.BoneTransforms(rig,Deformation.JointRotations(rig,pose));
var rotations=transforms.Rotations;
Deformation.ApplyTransforms(character,rig,transforms.Positions,rotations,deformed);
float stretch=1,minArea=1,sourceEdge=0,deformedEdge=0;int nonFinite=0,invalidMeasurements=0;
int reversed=0;double surfaceArea=0,reversedArea=0;
for(int p=0;p<character.Meshes.Length;p++)
{
var mesh=character.Meshes[p];var dst=deformed[p];nonFinite+=dst.Count(v=>!Geometry.Finite(v));
foreach(var face in faces[p])
{
var i=face.A;var j=face.B;var k=face.C;
var normal=face.Normal;
var posedNormal=Vector3.Cross(dst[j]-dst[i],dst[k]-dst[i]);
float area=face.Area,posedArea=posedNormal.Length();
if(!float.IsFinite(area)||!float.IsFinite(posedArea))invalidMeasurements++;
else if(area>height*height*1e-10f)
{
float ratio=posedArea/area;
if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements++;
surfaceArea+=area;
float alignment=SurfaceOrientation.Alignment(normal,posedNormal,rig.Weights[p][i],rig.Weights[p][j],rig.Weights[p][k],rotations);
if(alignment<SurfaceOrientation.ReversalLimit){reversed++;reversedArea+=area;}
}
for(int edge=0;edge<3;edge++)
{
var (a,b,length)=face.Edge(edge);var posedLength=Vector3.Distance(dst[a],dst[b]);
if(!float.IsFinite(length)||!float.IsFinite(posedLength)){invalidMeasurements++;continue;}
if(length<=height*1e-6f)continue;
float ratio=posedLength/length;
if(!float.IsFinite(ratio)){invalidMeasurements++;continue;}
if(ratio>stretch){stretch=ratio;sourceEdge=length;deformedEdge=posedLength;}
}
}
}
return new(pose.Name,stretch,minArea,nonFinite,sourceEdge,deformedEdge,invalidMeasurements,reversed,surfaceArea>0?(float)(reversedArea/surfaceArea):0);
}
}
Editor
library
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
/// <summary>Refine axial skinning with normal-aware heat and compact joint
/// support. Full deformation evidence must remain safe before accepting it.</summary>
internal static class TrunkSkinning
{
internal static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial)
{
var expected=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,rig.Bones.Select(b=>b.Role).ToHashSet())).Select(p=>p.Name).Order().ToArray();
if(!initial.Passed||!WeightRepair.HasCompleteEvidence(initial,expected))return initial;
var region=new TrunkRegion(character,rig);if(!region.HasBleeding(character,rig))return initial;
var original=rig.Weights;bool accepted=false;
try
{
var heat=HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:region).First();
var geometry=new ValidationGeometry(character,trunk:region);
foreach(float amount in new[]{1f,.5f,0f})
{
rig.Weights=Apply(character,rig,region,original,heat,amount);
var candidate=RigValidator.ValidateAndRepair(character,rig,geometry);
// Local surface repair may adjust a protected boundary. Never
// accept a trial that silently reintroduces remote attachments.
if(region.HasBleeding(character,rig)||!candidate.Passed||!WeightRepair.HasCompleteEvidence(candidate,expected)||
candidate.StressTests.Zip(initial.StressTests).Any(p=>p.First.Pose!=p.Second.Pose||p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))continue;
candidate.Repairs+=initial.Repairs;candidate.RepairPasses+=initial.RepairPasses+1;
accepted=true;return candidate;
}
}
catch(InvalidOperationException e){initial.Issues.Add(new("skinning-candidate",e.Message,false));}
finally{if(!accepted)rig.Weights=original;}
initial.Issues.Add(new("weight-bleeding","Torso skinning still follows a remote joint. Check the shoulder, hip and neck landmarks.",true));
return initial;
}
static Influence[][][] Apply(ImportedCharacter character,GeneratedRig rig,TrunkRegion region,Influence[][][] source,Influence[][][] heat,float amount)
{
var result=source.Select(p=>(Influence[][])p.Clone()).ToArray();var ends=RigGeometry.SegmentEnds(rig);
for(int p=0;p<result.Length;p++)for(int v=0;v<result[p].Length;v++)if(region.Vertices[p][v])
{
var point=character.Meshes[p].Vertices[v];float blend=amount*region.Blend(point);
var values=new float[rig.Bones.Length];
foreach(var w in source[p][v])values[w.Bone]+=w.Weight*(1-blend);
foreach(var w in heat[p][v])values[w.Bone]+=w.Weight*blend;
float removed=0;
foreach(var attachment in region.Attachments)
{
float support=attachment.Support(point);
for(int b=0;b<values.Length;b++)if(attachment.Moving[b]){removed+=values[b]*(1-support);values[b]*=support;}
}
float axial=heat[p][v].Where(w=>region.Axial[w.Bone]).Sum(w=>w.Weight);
if(axial>1e-6f)foreach(var w in heat[p][v]){if(region.Axial[w.Bone])values[w.Bone]+=removed*w.Weight/axial;}
else
{
int nearest=Enumerable.Range(0,rig.Bones.Length).Where(b=>region.Axial[b]).MinBy(b=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b])));
values[nearest]+=removed;
}
result[p][v]=Skinning.Cleanup(values,rig.Profile.MaximumInfluences);
}
return result;
}
}
Editor
library
using SkiaSharp;
namespace HumanoidRigger.Editor;
internal static partial class MaterialAssets
{
// Work on copies in the output/cache directory. glTF packs roughness in G
// and metalness in B; Source 2 and MTL consume separate grayscale maps.
internal static SourceMaterial[] PrepareFormats(SourceMaterial[] source,string directory,ExportFormats formats)
{
bool portable=(formats&(ExportFormats.Gltf|ExportFormats.Glb))!=0;
var converted=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);
string Png(string path)
{
if(path is null||Path.GetExtension(path).ToLowerInvariant() is ".png" or ".jpg" or ".jpeg")return path;
if(converted.TryGetValue(path,out var result))return result;
using var bitmap=Decode(path);result="textures/portable_"+converted.Count+".png";Save(bitmap,result);converted.Add(path,result);return result;
}
SKBitmap Decode(string path)=>SKBitmap.Decode(Path.Combine(directory,path))??throw new FormatException("Cannot decode texture '"+path+"'.");
void Save(SKBitmap bitmap,string path)
{
string output=Path.Combine(directory,path);Directory.CreateDirectory(Path.GetDirectoryName(output));
using var image=SKImage.FromBitmap(bitmap);using var data=image.Encode(SKEncodedImageFormat.Png,100);using var stream=File.Create(output);data.SaveTo(stream);
}
static SKColor Sample(SKBitmap bitmap,int x,int y,int width,int height,SKColor fallback)=>bitmap is null?fallback:bitmap.GetPixel(Math.Min(bitmap.Width-1,x*bitmap.Width/width),Math.Min(bitmap.Height-1,y*bitmap.Height/height));
static byte Channel(float value)=>(byte)Math.Clamp((int)MathF.Round(value),0,255);
return source.Select((original,index)=>
{
var m=ConvertSpecularGlossiness(original with{},directory,index);
if(m.AuthoredPbr||m.AuthoredEmission)
{
float peak=Math.Max(1,Math.Max(m.EmissiveFactor.X,Math.Max(m.EmissiveFactor.Y,m.EmissiveFactor.Z)));
m.EmissiveFactor/=peak;m.EmissiveStrength*=peak;
}
string WriteMap(string suffix,int width,int height,Func<int,int,SKColor> pixel)
{
using var bitmap=new SKBitmap(width,height,SKColorType.Rgba8888,SKAlphaType.Unpremul);
for(int y=0;y<height;y++)for(int x=0;x<width;x++)bitmap.SetPixel(x,y,pixel(x,y));
string path="textures/pbr_"+index+"_"+suffix+".png";Save(bitmap,path);return path;
}
if(m.AuthoredPbr)
{
using var packed=m.MetallicRoughnessTexture is null?null:Decode(m.MetallicRoughnessTexture);
int w=packed?.Width??1,h=packed?.Height??1;
m.RoughnessTexture=WriteMap("roughness",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Green??255)*m.RoughnessFactor);return new(v,v,v);});
m.MetalnessTexture=WriteMap("metalness",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Blue??255)*m.MetallicFactor);return new(v,v,v);});
}
else if(portable&&(m.RoughnessTexture is not null||m.MetalnessTexture is not null))
{
using var rough=m.RoughnessTexture is null?null:Decode(m.RoughnessTexture);using var metal=m.MetalnessTexture is null?null:Decode(m.MetalnessTexture);
int w=Math.Max(rough?.Width??1,metal?.Width??1),h=Math.Max(rough?.Height??1,metal?.Height??1);
m.MetallicRoughnessTexture=WriteMap("metallic_roughness",w,h,(x,y)=>new(255,Sample(rough,x,y,w,h,SKColors.White).Red,Sample(metal,x,y,w,h,SKColors.Black).Red));
}
if(m.NormalTexture is not null&&m.NormalScale!=1)
{
using var normal=Decode(m.NormalTexture);float strength=m.NormalScale;
m.NormalTexture=WriteMap("normal",normal.Width,normal.Height,(x,y)=>
{
var c=normal.GetPixel(x,y);
var n=new System.Numerics.Vector3((c.Red/255f*2-1)*strength,(c.Green/255f*2-1)*strength,c.Blue/255f*2-1);
n=n.LengthSquared()>1e-12f?System.Numerics.Vector3.Normalize(n):System.Numerics.Vector3.UnitZ;
return new(Channel((n.X*.5f+.5f)*255),Channel((n.Y*.5f+.5f)*255),Channel((n.Z*.5f+.5f)*255),c.Alpha);
});
m.NormalScale=1;
}
if(m.AuthoredPbr&&m.OcclusionTexture is not null)
{
// glTF AO uses only R, even when roughness and metalness share
// the same image. Source 2 needs a separate grayscale input.
using var occlusion=Decode(m.OcclusionTexture);float strength=m.OcclusionStrength;
m.OcclusionTexture=WriteMap("occlusion",occlusion.Width,occlusion.Height,(x,y)=>
{byte v=Channel(255+strength*(occlusion.GetPixel(x,y).Red-255));return new(v,v,v);});
m.OcclusionStrength=1;
}
if((m.AuthoredPbr||m.AuthoredEmission)&&m.EmissiveTexture is null&&m.EmissiveFactor.LengthSquared()>0&&m.EmissiveStrength>0)
m.EmissiveTexture=WriteMap("emission",1,1,(x,y)=>SKColors.White);
if(portable)
{
if(m.OpacityTexture is not null)
{
using var color=m.ColorTexture is null?null:Decode(m.ColorTexture);using var opacity=Decode(m.OpacityTexture);
int w=Math.Max(color?.Width??1,opacity.Width),h=Math.Max(color?.Height??1,opacity.Height);
bool packedAlpha=string.Equals(m.OpacityTexture,m.ColorTexture,StringComparison.OrdinalIgnoreCase);
m.ColorTexture=WriteMap("rgba",w,h,(x,y)=>{var c=Sample(color,x,y,w,h,SKColors.White);var a=Sample(opacity,x,y,w,h,SKColors.White);return new(c.Red,c.Green,c.Blue,packedAlpha?a.Alpha:Channel(c.Alpha*a.Red/255f));});
}
m.ColorTexture=Png(m.ColorTexture);m.NormalTexture=Png(m.NormalTexture);m.MetallicRoughnessTexture=Png(m.MetallicRoughnessTexture);m.OcclusionTexture=Png(m.OcclusionTexture);m.EmissiveTexture=Png(m.EmissiveTexture);
}
return m;
}).ToArray();
}
}
Debug: View Raw JSON Response
{
"TotalCount": 110,
"Files": [
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/UI/AssetOutput.cs",
"FileName": "AssetOutput.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "using Editor;\r\nusing Sandbox;\r\nusing System.Threading.Tasks;\r\nnamespace HumanoidRigger.Editor;\r\n\r\npublic sealed record ExportResult(string[] Files)\r\n{\r\n public string PrimaryFile=>Files.LastOrDefault(p=>p.EndsWith(\".vmdl\",StringComparison.OrdinalIgnoreCase))??Files.First();\r\n}\r\n\r\npublic static class AssetOutput\r\n{\r\n public static async Task<string> Save(Wizard session)=>\r\n (await Save(session,ExportRequest.Default(session.Character!.Name,Project.Current.GetAssetsPath(),session.Character.Materials.Length>0))).PrimaryFile;\r\n\r\n public static async Task<ExportResult> Save(Wizard session,ExportRequest request)\r\n {\r\n if(session.Rig?.Report.Passed!=true)throw new InvalidOperationException(\"The rig must pass validation before saving.\");\r\n var character=session.Character!;var rig=session.Rig;\r\n if(request.Formats.HasFlag(ExportFormats.Vmdl))VmdlBoneNames.Validate(rig.Bones);\r\n var root=Project.Current.GetAssetsPath();var plan=request.Plan(root,character.Materials.Length>0);plan.EnsureAvailable();\r\n Directory.CreateDirectory(plan.Directory);\r\n var written=new List<string>();bool createdMaterials=false;\r\n void Write(string path,byte[] bytes)\r\n {\r\n using var file=new FileStream(path,FileMode.CreateNew,FileAccess.Write,FileShare.Read);written.Add(path);file.Write(bytes);\r\n }\r\n void WriteText(string path,string content)=>Write(path,System.Text.Encoding.UTF8.GetBytes(content));\r\n try\r\n {\r\n var sourceMaterials=character.Materials;\r\n IReadOnlyDictionary<int,string> vmats=new Dictionary<int,string>();\r\n if(plan.MaterialDirectory is not null)\r\n {\r\n Directory.CreateDirectory(plan.MaterialDirectory);createdMaterials=true;\r\n sourceMaterials=await Task.Run(()=>TextureFiles.Copy(character,plan.MaterialDirectory));await new EditorThread();\r\n sourceMaterials=await Task.Run(()=>MaterialAssets.PrepareFormats(sourceMaterials,plan.MaterialDirectory,plan.Formats));await new EditorThread();\r\n if(plan.Formats.HasFlag(ExportFormats.Vmdl))\r\n {\r\n vmats=await MaterialAssets.Compile(sourceMaterials,plan.MaterialDirectory,character);\r\n }\r\n }\r\n var portableMaterials=plan.MaterialDirectory is null?sourceMaterials:TextureFiles.RelativeTo(sourceMaterials,Path.GetFileName(plan.MaterialDirectory));\r\n if(plan.Formats.HasFlag(ExportFormats.Fbx))\r\n {\r\n var bytes=await Task.Run(()=>FbxExporter.Write(character,rig,portableMaterials));await new EditorThread();\r\n Write(plan.PathFor(\".fbx\"),bytes);\r\n if(ExportRequest.IsInAssets(plan.Directory,root))AssetSystem.RegisterFile(plan.PathFor(\".fbx\"));\r\n }\r\n if(plan.Formats.HasFlag(ExportFormats.Gltf))\r\n {\r\n var output=await Task.Run(()=>GltfExporter.Write(character,rig,plan.FileName+\".bin\",portableMaterials));await new EditorThread();\r\n Write(plan.PathFor(\".bin\"),output.Buffer);Write(plan.PathFor(\".gltf\"),output.Document);\r\n }\r\n if(plan.Formats.HasFlag(ExportFormats.Glb))\r\n {\r\n var output=await Task.Run(()=>GltfExporter.Write(character,rig,\"\",portableMaterials,true,path=>File.ReadAllBytes(Path.Combine(plan.Directory,path))));await new EditorThread();\r\n Write(plan.PathFor(\".glb\"),output.Document);\r\n }\r\n if(plan.Formats.HasFlag(ExportFormats.Obj))\r\n {\r\n var output=await Task.Run(()=>ObjExporter.Write(character,plan.FileName+\".mtl\",portableMaterials));await new EditorThread();\r\n WriteText(plan.PathFor(\".mtl\"),output.Materials);WriteText(plan.PathFor(\".obj\"),output.Mesh);\r\n }\r\n if(plan.Formats.HasFlag(ExportFormats.Vmdl))\r\n {\r\n var dmx=await Task.Run(()=>DmxExporter.Write(character,rig,vmats));await new EditorThread();\r\n WriteText(plan.PathFor(\".dmx\"),dmx);\r\n WriteText(plan.PathFor(\".vmdl\"),ModelDocExporter.Write(Path.GetRelativePath(root,plan.PathFor(\".dmx\")),rig,character,vmats.Count>0));\r\n await NativeRigExport.Compile(plan.PathFor(\".dmx\"),plan.PathFor(\".vmdl\"),character,rig,vmats,\r\n content=>File.WriteAllText(plan.PathFor(\".dmx\"),content));\r\n }\r\n return new(plan.PrimaryFiles);\r\n }\r\n catch\r\n {\r\n // Only remove output paths this attempt created. Existing user assets are never overwritten.\r\n foreach(var path in written)foreach(var owned in new[]{path,path+\"_c\"})try{File.Delete(owned);}catch(Exception e){Log.Warning(e.Message);}\r\n if(createdMaterials&&ExportRequest.IsInAssets(plan.MaterialDirectory,plan.Directory))\r\n try{Directory.Delete(plan.MaterialDirectory,true);}catch(Exception e){Log.Warning(e.Message);}\r\n throw;\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/UI/RiggerWindow.cs",
"FileName": "RiggerWindow.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "using Editor;\r\nusing Sandbox;\r\nusing System.Threading.Tasks;\r\nnamespace HumanoidRigger.Editor;\r\n\r\npublic sealed class RiggerWindow : Widget\r\n{\r\n public const string DockTitle=\"Humanoid Rigger\";\r\n public static RiggerWindow Instance {get;private set;}\r\n public Wizard Session {get;}=new();\r\n RiggerViewport viewport;Widget content,toolbar,footer,toolbarContent,footerContent;Label status;bool busy;\r\n ImportedCharacter framedCharacter;WizardStep framedStep;\r\n JointPanel jointPanel;\r\n CharacterPose previewPose=CharacterPose.Auto;\r\n ComboBox poseSelection;\r\n bool customPose;\r\n bool updatingPoseSelection;\r\n IReadOnlyDictionary<string,System.Numerics.Quaternion> editedPose;\r\n AutomaticHandRefiner handRefiner;\r\n IReadOnlyDictionary<int,string> previewMaterials=new Dictionary<int,string>();\r\n internal double LastAdvanceWorkMilliseconds {get;private set;}\r\n internal double LastAdvanceUiMilliseconds {get;private set;}\r\n internal bool LastAdvanceWorkerWasMainThread {get;private set;}\r\n Task<Wizard> preparation;\r\n Wizard preparingDraft;\r\n long queuedPreparationRevision=-1;\r\n FingerCountDialog fingerCountDialog;\r\n public RiggerWindow(Widget parent):this(parent,true){}\r\n internal RiggerWindow(Widget parent,bool register):base(parent)\r\n {\r\n if(register)Instance=this;WindowTitle=DockTitle;Name=\"HumanoidRigger\";Cursor=CursorShape.Arrow;SetWindowIcon(\"accessibility_new\");MinimumSize=new(1060,780);Size=new(1280,940);Layout=Layout.Column();EnsureHandRefiner();Build();\r\n }\r\n [Event(\"tools.editorwindow.createview\")]\r\n static void RegisterViewMenu(Menu menu)\r\n {\r\n // Join the editor's alphabetically sorted tools without creating a dock.\r\n EditorWindow.DockManager.RegisterDockType(new DockManager.DockInfo\r\n {\r\n Title=DockTitle,Icon=\"accessibility_new\",CreateAction=OpenFloatingView\r\n });\r\n }\r\n static Widget OpenFloatingView(){Open();return null;}\r\n\r\n [Event(\"tools.editorwindow.postcreateview\")]\r\n static void ConfigureViewMenu(Menu menu)\r\n {\r\n var option=menu.GetOption(DockTitle);\r\n if(option is null)return;\r\n option.Toggled=null;option.Checkable=false;option.Triggered=Open;\r\n }\r\n\r\n public static void Open()\r\n {\r\n if(Instance.IsValid()){ShowExistingWindow(Instance);return;}\r\n CreateFloatingWindow(DockTitle,true);\r\n }\r\n internal static void ShowExistingWindow(RiggerWindow window)\r\n {\r\n // Move sessions opened by older versions into the same themed window as new\r\n // sessions. Preserve the widget, viewport and edits when removing the old dock.\r\n var dock=EditorWindow.DockManager.FindDockWidget(window);\r\n if(dock.IsValid())\r\n {\r\n var previous=window.GetWindow();var size=previous.Size;var position=previous.Position;bool floating=dock.IsFloating;\r\n var dialog=CreateFloatingHost(window.WindowTitle);\r\n // Replacing the dock content releases its native ownership before reparenting.\r\n dock.Widget=new Widget(dock);\r\n window.Parent=dialog;dialog.Layout.Add(window,1);\r\n EditorWindow.DockManager.RemoveDock(dock);dock.Destroy();\r\n if(floating){dialog.Window.Size=size;dialog.Window.Position=position;}\r\n dialog.Show();window.Show();dialog.Window.Raise();\r\n return;\r\n }\r\n var existing=window.GetWindow();existing.Show();window.Show();existing.Raise();\r\n }\r\n internal static Dialog CreateFloatingWindow(string title,bool register)\r\n {\r\n var dialog=CreateFloatingHost(title);\r\n dialog.Layout.Add(new RiggerWindow(dialog,register),1);dialog.Show();dialog.Window.Size=new(1280,940);return dialog;\r\n }\r\n void Build()\r\n {\r\n if(Session.Rig is null){previewPose=CharacterPose.Auto;customPose=false;editedPose=null;}\r\n if(Session.Step==WizardStep.Import)\r\n {\r\n content?.Destroy();viewport=null;framedCharacter=null;\r\n content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();\r\n var drop=new ModelDropArea(content,ImportPath);content.Layout.Add(drop,1);status=content.Layout.Add(new Label(content){Visible=false,WordWrap=true});return;\r\n }\r\n if(!viewport.IsValid()||!toolbar.IsValid()||!footer.IsValid()||!jointPanel.IsValid())\r\n {\r\n if(viewport.IsValid())viewport.Parent=this;\r\n content?.Destroy();content=Layout.Add(new Widget(this),1);content.Layout=Layout.Column();\r\n toolbar=content.Layout.Add(new Widget(content));toolbar.Layout=Layout.Column();\r\n var middle=content.Layout.Add(new Widget(content),1);middle.Layout=Layout.Column();\r\n viewport=middle.Layout.Add(viewport.IsValid()?viewport:new RiggerViewport(middle,Session),1);\r\n // Overlay the panel so entering Body does not resize the native\r\n // render surface. The viewport frames the character beside it.\r\n jointPanel=new JointPanel(viewport,Session,viewport);viewport.Changed=LandmarksChanged;\r\n // Reserve the final review's space throughout the wizard. Changing\r\n // warnings or progress text must not repeatedly resize the swap chain.\r\n footer=content.Layout.Add(new Widget(content){FixedHeight=160});footer.Layout=Layout.Column();\r\n }\r\n viewport.PoseEdited=PoseEdited;\r\n viewport.Resized=PositionJointPanel;\r\n toolbarContent?.Destroy();footerContent?.Destroy();\r\n toolbarContent=toolbar.Layout.Add(new Widget(toolbar));toolbarContent.Layout=Layout.Row();\r\n footerContent=footer.Layout.Add(new Widget(footer));footerContent.Layout=Layout.Column();\r\n var top=toolbarContent.Layout;top.Margin=8;top.Spacing=8;\r\n top.Add(new Label(content){Text=\"Profile:\"});\r\n var profile=top.Add(new ComboBox(content){MinimumWidth=190,ToolTip=\"The target skeleton\u2019s bone names and hierarchy. Hand detection is independent of this choice.\"});\r\n foreach(var p in Profiles.BuiltIn.Concat(ProfileStore.Load()))profile.AddItem(p.Name,\"person\",()=>{Session.SetProfile(p);Build();},selected:p.Id==Session.Profile.Id);\r\n profile.AddItem(\"Create Custom Profile\u2026\",\"add\",CreateProfile);\r\n profile.AddItem(\"Load Profile\u2026\",\"folder_open\",LoadProfile);\r\n if(Session.Rig is not null)\r\n {\r\n top.Add(new Label(content){Text=\"Preview:\"});\r\n poseSelection=top.Add(new ComboBox(content){MinimumWidth=120,ToolTip=\"Preview the generated rig in a standard pose.\"});\r\n foreach(var (label,value) in new[]{(\"Original Pose\",CharacterPose.Auto),(\"T-Pose\",CharacterPose.TPose),(\"A-Pose 1\",CharacterPose.APose1),(\"A-Pose 2\",CharacterPose.APose2)})\r\n poseSelection.AddItem(label,\"accessibility\",()=>SetPreviewPose(value),selected:!customPose&&value==previewPose);\r\n if(editedPose is not null)poseSelection.AddItem(\"Custom Pose\",\"touch_app\",ShowEditedPose,selected:customPose);\r\n }\r\n top.AddStretchCell();top.Add(new Button(\"Replace model\",\"folder_open\"){Clicked=SelectModel});top.Add(new Button(\"Restart\",\"restart_alt\"){Clicked=RestartWorkflow});\r\n viewport.MaterialPaths=previewMaterials;ShowPreview();\r\n jointPanel.Visible=Session.Step!=WizardStep.Centerline;\r\n PositionJointPanel();\r\n jointPanel.Reload();\r\n if(framedCharacter!=Session.Character||framedStep!=Session.Step){viewport.Frame();framedCharacter=Session.Character;framedStep=Session.Step;}\r\n var bottom=footerContent.Layout;bottom.Margin=12;bottom.Spacing=8;\r\n status=bottom.Add(new Label(content){WordWrap=true,Text=Instruction()});\r\n if(Session.Step is WizardStep.Centerline or WizardStep.Body)\r\n {\r\n var importWarnings=Session.Character.ImportWarnings.Where(w=>!w.StartsWith(\"Detected a Z-up\")).ToArray();\r\n if(importWarnings.Length>0)\r\n {\r\n var warning=bottom.Add(new Label(content){Name=\"ImportMaterialWarning\",Text=string.Join(\"\\n\",importWarnings),WordWrap=true});\r\n warning.SetStyles($\"color: {Theme.Yellow.Hex};\");\r\n }\r\n }\r\n if(Session.Step is WizardStep.Centerline or WizardStep.Body && Session.Anatomy!.UnrecommendedImportPose)\r\n {\r\n var warning=bottom.Add(new Label(content){Name=\"ImportPoseWarning\",Text=ImportPose.Warning,WordWrap=true});\r\n warning.SetStyles($\"color: {Theme.Yellow.Hex};\");\r\n }\r\n if(Session.Step is WizardStep.LeftHand or WizardStep.RightHand)\r\n {\r\n var side=Session.Step==WizardStep.LeftHand?\"L\":\"R\";\r\n foreach(var warning in Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+\" hand\")||w.StartsWith(side+\" fingers\")))\r\n {var label=bottom.Add(new Label(content){Text=warning,WordWrap=true});label.SetStyles($\"color: {Theme.Yellow.Hex};\");}\r\n }\r\n if(Session.Step==WizardStep.Finish)\r\n {\r\n bottom.Add(new Label(content){Text=$\"Profile: {Session.Profile.Name}\"});\r\n var deformationWarnings=Session.Rig!.Report.Issues.Where(i=>i.Code==\"surface-reversal\").Select(i=>i.Message).ToArray();\r\n if(deformationWarnings.Length>0)\r\n {\r\n var warning=bottom.Add(new Label(content){Name=\"DeformationWarning\",Text=\"Some test poses need review. Open Advanced Edit for details.\",WordWrap=true});\r\n warning.SetStyles($\"color: {Theme.Yellow.Hex};\");\r\n }\r\n var checks=bottom.AddRow();checks.Spacing=8;\r\n foreach(var label in new[]{\"Body\",\"Left Hand\",\"Right Hand\",\"Skeleton\",\"Skinning\",\"Validation\"})\r\n {\r\n string side=label==\"Left Hand\"?\"L\":label==\"Right Hand\"?\"R\":null;\r\n var warnings=label==\"Validation\"?deformationWarnings:side is null?[]:Session.Anatomy!.Warnings.Where(w=>w.StartsWith(side+\" hand\")||w.StartsWith(side+\" fingers\")).ToArray();\r\n checks.Add(new StatusChip(content,label,warnings.Length>0?Theme.Yellow:Theme.Green){ToolTip=warnings.Length>0?string.Join(\"\\n\",warnings):\"Checked\"});\r\n }\r\n checks.AddStretchCell();\r\n var row=bottom.AddRow();row.Spacing=8;\r\n row.Add(new Button(\"Back\",\"arrow_back\"){Clicked=GoBack});\r\n row.Add(new Button(\"Reset Pose\",\"restart_alt\"){Clicked=()=>SetPreviewPose(CharacterPose.Auto)});\r\n row.Add(new Button(\"Test Rig\",\"play_arrow\"){Clicked=TestRig});row.Add(new Button(\"Advanced Edit\",\"tune\"){Clicked=Advanced});row.AddStretchCell();row.Add(new Button.Primary(\"Save\"){Icon=\"check\",Tint=Theme.Green,Clicked=Save});\r\n }\r\n else\r\n {\r\n var row=bottom.AddRow();row.Spacing=8;\r\n if(Session.Step!=WizardStep.Centerline)row.Add(new Button(\"Back\",\"arrow_back\"){Clicked=GoBack});\r\n row.Add(new Button(\"Reset\",\"restart_alt\"){Clicked=()=>{Session.Reset();Build();}});row.AddStretchCell();\r\n row.Add(new Button.Primary(\"Continue\"){Enabled=Session.Step!=WizardStep.Validation,Clicked=Continue});\r\n }\r\n SchedulePreparation();\r\n }\r\n void PositionJointPanel()\r\n {\r\n if(!viewport.IsValid()||!jointPanel.IsValid())return;\r\n jointPanel.Position=new(viewport.Width-jointPanel.FixedWidth,0);\r\n jointPanel.Size=new(jointPanel.FixedWidth,viewport.Height);\r\n viewport.RightInset=jointPanel.Visible?jointPanel.FixedWidth:0;\r\n jointPanel.Raise();\r\n }\r\n void GoBack(){Session.Back();Build();}\r\n string Instruction()=>Session.Step switch\r\n {\r\n WizardStep.Centerline=>\"Check the centerline.\\nDrag the line left or right to adjust it.\",\r\n WizardStep.Body=>\"Check the points.\\nMove any point that is incorrect.\",WizardStep.LeftHand=>\"Check the left hand.\\nMove any incorrect points.\",WizardStep.RightHand=>\"Check the right hand.\\nMove any incorrect points.\",WizardStep.Finish=>\"Rig Complete\\nDrag a bone to test the rig.\",\r\n WizardStep.Validation=>string.Join(\"\\n\",Session.Rig!.Report.Issues.Where(i=>i.Error).Select(i=>i.Message)),_=>\"Generating rig\u2026\"\r\n };\r\n void SelectModel(){var path=EditorUtility.OpenFileDialog(\"Select model\",ModelImporter.FileFilter,\"\");if(!string.IsNullOrEmpty(path))ImportPath(path);}\r\n void CreateProfile()\r\n {\r\n var path=EditorUtility.OpenFileDialog(\"Select rigged character\",\"Rigged characters (*.fbx *.gltf *.glb)\",\"\");if(string.IsNullOrEmpty(path))return;\r\n try{new ProfileDialog(this,ModelImporter.Import(path),p=>{Session.SetProfile(p);Build();}).Show();}catch(Exception e){Error(e);}\r\n }\r\n void LoadProfile()\r\n {\r\n var path=EditorUtility.OpenFileDialog(\"Select rig profile\",\"json\",\"\");if(string.IsNullOrEmpty(path))return;\r\n try{var p=RigProfile.FromJson(File.ReadAllText(path));ProfileStore.Save(p);Session.SetProfile(p);Build();}catch(Exception e){Error(e);}\r\n }\r\n public async void ImportPath(string path)\r\n {\r\n try{await ImportAsync(path);}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}\r\n }\r\n public async Task ImportAsync(string path)\r\n {\r\n if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n EnsureHandRefiner();\r\n SetBusy(true);\r\n try\r\n {\r\n await RigWork.Run(()=>Session.Import(path));await new EditorThread();\r\n if(!this.IsValid())return;\r\n previewMaterials=new Dictionary<int,string>();\r\n string textureWarning=null;\r\n try{previewMaterials=await MaterialAssets.Preview(Session.Character);}\r\n catch(Exception e){textureWarning=\"Textures: \"+e.Message;Log.Warning(textureWarning);}\r\n await new EditorThread();if(this.IsValid()){Build();if(textureWarning is not null){status.Text+=\"\\n\"+textureWarning;status.SetStyles($\"color: {Theme.Yellow.Hex};\");}}\r\n }\r\n finally{await new EditorThread();SetBusy(false);}\r\n }\r\n void Continue()\r\n {\r\n if(busy)return;\r\n if(Session.Step==WizardStep.Body)\r\n {\r\n if(fingerCountDialog.IsValid()&&fingerCountDialog.Visible){fingerCountDialog.Window.Raise();return;}\r\n var revision=Session.Revision;\r\n fingerCountDialog=new FingerCountDialog(this,Session.LeftFingerCount,Session.RightFingerCount,(left,right)=>\r\n {\r\n if(!this.IsValid()||Session.Step!=WizardStep.Body||Session.Revision!=revision)return;\r\n Session.SetFingerCounts(left,right);AdvanceFromControls();\r\n });\r\n fingerCountDialog.Show();return;\r\n }\r\n AdvanceFromControls();\r\n }\r\n async void AdvanceFromControls()\r\n {\r\n try{await AdvanceAsync();}catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}\r\n }\r\n public async Task AdvanceAsync()\r\n {\r\n if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n EnsureHandRefiner();\r\n SetBusy(true);\r\n status.Text=Session.Step switch{WizardStep.Body=>\"Preparing left hand\u2026\",WizardStep.LeftHand=>\"Preparing right hand\u2026\",WizardStep.RightHand=>\"Generating rig\u2026\",_=>Instruction()};\r\n try\r\n {\r\n var started=System.Diagnostics.Stopwatch.GetTimestamp();\r\n if(preparation is not null&&!preparation.IsCompleted&&!Session.CanAccept(preparingDraft))\r\n {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}\r\n Wizard result;\r\n if(Session.Step==WizardStep.RightHand)\r\n {\r\n // Give the busy state a frame to appear, then generate on the\r\n // editor thread. Keep a draft so a failure preserves the edits.\r\n await Task.Delay(16).ConfigureAwait(false);await new EditorThread();\r\n if(!this.IsValid())return;\r\n result=Session.CopyForContinuation();\r\n LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;\r\n result.Continue();\r\n }\r\n else\r\n {\r\n if(preparation is null||!Session.CanAccept(preparingDraft)||preparation.IsFaulted)StartPreparation();\r\n result=await preparation.ConfigureAwait(false);await new EditorThread();\r\n }\r\n Session.AcceptContinuation(result);\r\n LastAdvanceWorkMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;\r\n started=System.Diagnostics.Stopwatch.GetTimestamp();if(this.IsValid())Build();\r\n LastAdvanceUiMilliseconds=System.Diagnostics.Stopwatch.GetElapsedTime(started).TotalMilliseconds;\r\n }\r\n finally{await new EditorThread();SetBusy(false);SchedulePreparation();}\r\n }\r\n public void CaptureViewport(string path)=>viewport.Capture(path);\r\n internal void RefreshDisplay()\r\n {\r\n // Rebind native event handlers after editor hot reload without replacing the session.\r\n viewport?.Destroy();viewport=null;framedCharacter=null;MinimumSize=new(1060,780);Build();\r\n }\r\n void ShowPreview()\r\n {\r\n if(Session.Rig is null){viewport.ShowCharacter();return;}\r\n viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose),false);\r\n }\r\n public void SetPreviewPose(CharacterPose pose)\r\n {\r\n if(updatingPoseSelection)return;\r\n if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n if(Session.Rig is null)throw new InvalidOperationException(\"Generate the rig before previewing poses.\");\r\n if(pose is not (CharacterPose.Auto or CharacterPose.TPose or CharacterPose.APose1 or CharacterPose.APose2))throw new ArgumentException(\"Unsupported preview pose.\");\r\n previewPose=pose;customPose=false;viewport.SetPose(RigPosePreview.Rotations(Session.Rig,pose));\r\n var label=pose switch{CharacterPose.Auto=>\"Original Pose\",CharacterPose.TPose=>\"T-Pose\",CharacterPose.APose1=>\"A-Pose 1\",_=>\"A-Pose 2\"};\r\n SelectPoseLabel(label);\r\n status.Text=Instruction();\r\n }\r\n void PoseEdited(IReadOnlyDictionary<string,System.Numerics.Quaternion> pose)\r\n {\r\n customPose=true;editedPose=new Dictionary<string,System.Numerics.Quaternion>(pose);\r\n SelectPoseLabel(\"Custom Pose\");\r\n }\r\n void SelectPoseLabel(string label)\r\n {\r\n if(!poseSelection.IsValid())return;\r\n // Native ComboBox selection invokes its action even for programmatic\r\n // changes. Updating the label must not restart or cancel an active drag.\r\n updatingPoseSelection=true;\r\n try\r\n {\r\n if(poseSelection.FindIndex(label) is {} index)poseSelection.CurrentIndex=index;\r\n else if(label==\"Custom Pose\")poseSelection.AddItem(label,\"touch_app\",ShowEditedPose,selected:true);\r\n }\r\n finally{updatingPoseSelection=false;}\r\n }\r\n void ShowEditedPose(){if(updatingPoseSelection||editedPose is null||Session.Rig is null)return;customPose=true;viewport.SetPose(editedPose);}\r\n public void RestartWorkflow()\r\n {\r\n if(busy)throw new InvalidOperationException(\"The current operation is still running.\");\r\n Session.Restart();Build();\r\n }\r\n void Error(Exception e){status.Visible=true;status.Text=e.Message;status.SetStyles($\"color: {Theme.Red.Hex};\");}\r\n void SetBusy(bool value)\r\n {\r\n busy=value;if(!this.IsValid())return;\r\n if(viewport.IsValid())\r\n {\r\n viewport.AllowEditing=!value;\r\n if(toolbar.IsValid())toolbar.Enabled=!value;if(footer.IsValid())footer.Enabled=!value;if(jointPanel.IsValid())jointPanel.Enabled=!value;\r\n }\r\n else if(content.IsValid())content.Enabled=!value;\r\n }\r\n async void TestRig()\r\n {\r\n if(busy)return;SetBusy(true);\r\n try\r\n {\r\n foreach(var pose in Deformation.Poses)\r\n {\r\n await new EditorThread();if(!this.IsValid()||Session.Rig is null)break;\r\n if(!Deformation.IsApplicable(pose,Session.Rig.Bones.Select(b=>b.Role).ToHashSet()))continue;\r\n viewport.SetPose(Deformation.JointRotations(Session.Rig,pose));status.Text=pose.Name;await Task.Delay(650);\r\n }\r\n await new EditorThread();if(this.IsValid()){viewport.SetPose(customPose?editedPose:RigPosePreview.Rotations(Session.Rig,previewPose));status.Text=Instruction();}\r\n }\r\n catch(Exception e){await new EditorThread();if(this.IsValid())Error(e);}finally{await new EditorThread();SetBusy(false);}\r\n }\r\n void Advanced()\r\n {\r\n var dialog=new Dialog(this);dialog.Window.WindowTitle=\"Rig details\";dialog.Window.MinimumSize=new(560,480);dialog.Layout=Layout.Column();dialog.Layout.Margin=12;dialog.Layout.Spacing=8;\r\n var scroll=new ScrollArea(dialog);scroll.Canvas=new Widget(scroll);scroll.Canvas.Layout=Layout.Column();\r\n foreach(var b in Session.Rig!.Bones)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$\"{b.Name} \u00b7 {b.Role} \u00b7 {b.Position}\"});\r\n foreach(var issue in Session.Rig.Report.Issues)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=issue.Message});\r\n foreach(var hand in Session.Anatomy!.HandRefinements)scroll.Canvas.Layout.Add(new Label(scroll.Canvas){Text=$\"{(hand.Key==\"L\"?\"Left\":\"Right\")} hand: {hand.Value.Status}\"});\r\n dialog.Layout.Add(scroll,1);dialog.Show();\r\n }\r\n void Save()\r\n {\r\n if(busy)return;\r\n new SaveRigDialog(this,Session,result=>{if(this.IsValid()){status.Visible=true;status.Text=\"Saved \"+string.Join(\" + \",result.Files.Select(Path.GetFileName));status.ToolTip=string.Join(\"\\n\",result.Files);status.SetStyles($\"color: {Theme.Green.Hex};\");}}).Show();\r\n }\r\n void EnsureHandRefiner(){if(handRefiner is null){handRefiner=new AutomaticHandRefiner();handRefiner.Warmup();}Session.HandRefiner=handRefiner;}\r\n public override void OnDestroyed(){handRefiner?.Dispose();if(Instance==this)Instance=null;base.OnDestroyed();}\r\n static Dialog CreateFloatingHost(string title)\r\n {\r\n var dialog=new Dialog(null);dialog.Window.Title=title;dialog.Window.SetWindowIcon(\"accessibility_new\");\r\n dialog.Layout=Layout.Column();dialog.Window.Size=new(1280,940);return dialog;\r\n }\r\n void LandmarksChanged(){jointPanel.RefreshRows();SchedulePreparation();}\r\n void StartPreparation()\r\n {\r\n preparingDraft=Session.CopyForContinuation();var draft=preparingDraft;\r\n preparation=RigWork.Run(()=>{LastAdvanceWorkerWasMainThread=ThreadSafe.IsMainThread;draft.Continue();return draft;});\r\n _=preparation.ContinueWith(task=>{_ = task.Exception;},TaskContinuationOptions.OnlyOnFaulted);\r\n }\r\n void SchedulePreparation()\r\n {\r\n // Preparing hands is cheap; generating a full rig while its final hand\r\n // is still being edited wastes both memory and a complete repair pass.\r\n if(!this.IsValid()||Session.Step is not (WizardStep.Body or WizardStep.LeftHand)||queuedPreparationRevision==Session.Revision)return;\r\n queuedPreparationRevision=Session.Revision;_=PrepareWhenIdle(Session.Revision);\r\n }\r\n async Task PrepareWhenIdle(long revision)\r\n {\r\n // Coalesce marker drags. At most one solver job per window may run at a time.\r\n await Task.Delay(150).ConfigureAwait(false);await new EditorThread();\r\n if(!this.IsValid()||busy||Session.Revision!=revision)return;\r\n if(preparation is not null&&!preparation.IsCompleted)\r\n {try{await preparation.ConfigureAwait(false);}catch{}await new EditorThread();}\r\n if(!this.IsValid()||busy||Session.Revision!=revision)return;\r\n if(preparingDraft is null||!Session.CanAccept(preparingDraft))StartPreparation();\r\n }\r\n}\r\n\r\nsealed class ModelDropArea:Widget\r\n{\r\n readonly Action<string> import;int hover;\r\n public ModelDropArea(Widget parent,Action<string> import):base(parent)\r\n {\r\n this.import=import;AcceptDrops=true;Layout=Layout.Column();Layout.Margin=12;Layout.Spacing=8;Layout.AddStretchCell();\r\n var row=Layout.AddRow();row.AddStretchCell();var center=row.AddColumn();center.Spacing=12;\r\n center.Add(new DropFolderIcon(this));\r\n center.Add(new Label(this){Text=\"Please drag and drop a character file here (.fbx, .obj, .gltf, .glb)\",Alignment=TextFlag.Center});\r\n center.Add(new Label(this){Text=\"or\",Alignment=TextFlag.Center});\r\n var choice=center.AddRow();choice.AddStretchCell();\r\n choice.Add(new Button.Primary(\"Choose File\"){MinimumWidth=120,Clicked=()=>{var path=EditorUtility.OpenFileDialog(\"Choose File\",ModelImporter.FileFilter,\"\");if(!string.IsNullOrEmpty(path))import(path);}});\r\n choice.AddStretchCell();\r\n row.AddStretchCell();Layout.AddStretchCell();\r\n }\r\n public override void OnDragHover(DragEvent e)\r\n {\r\n bool valid=e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder);hover=valid?1:-1;if(valid)e.Action=DropAction.Link;Update();\r\n }\r\n public override void OnDragDrop(DragEvent e){hover=0;if(e.Data.HasFileOrFolder&&ModelImporter.CanImport(e.Data.FileOrFolder)){e.Action=DropAction.Link;import(e.Data.FileOrFolder);}Update();}\r\n public override void OnDragLeave(){hover=0;Update();}\r\n protected override void OnPaint(){Paint.SetPen(hover==1?Theme.Green:hover<0?Theme.Red:Theme.ControlBackground.Lighten(.2f),1);Paint.SetBrush(hover==1?Theme.Green.WithAlpha(.06f):Paint.HasMouseOver?Theme.ControlBackground.Lighten(.3f):Theme.ControlBackground);Paint.DrawRect(LocalRect.Shrink(12),4);}\r\n}\r\n\r\nsealed class DropFolderIcon : Widget\r\n{\r\n public DropFolderIcon(Widget parent):base(parent){FixedHeight=48;}\r\n protected override void OnPaint()\r\n {\r\n Paint.SetPen(Theme.TextLight);\r\n Paint.DrawIcon(new Rect((Width-40)*.5f,4,40,40),\"create_new_folder\",40);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/BodyProportions.cs",
"FileName": "BodyProportions.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>A low, narrow neck beneath an enlarged head provides a body-scale\r\n/// prior independent of total height. Ambiguous sections retain the original prior.</summary>\r\ninternal static class BodyProportions\r\n{\r\n readonly record struct Section(float Y,float Width,float Depth)\r\n {\r\n public float Area=>Width*Depth;\r\n }\r\n public static float EstimateBodyHeight(IEnumerable<MeshPart> source,float bottom,float height)\r\n {\r\n var meshes=source.ToArray();var sections=new List<Section>();\r\n // Intersect triangle edges rather than sampling vertices: dense faces,\r\n // sparse neck rings and material seams should give the same cross-section.\r\n for(int sample=0;sample<=128;sample++)\r\n {\r\n float y=bottom+height*(.5f+sample*.0035f);\r\n float minX=float.PositiveInfinity,maxX=float.NegativeInfinity,minZ=float.PositiveInfinity,maxZ=float.NegativeInfinity;\r\n int count=0;\r\n foreach(var mesh in meshes)for(int t=0;t<mesh.Triangles.Length;t+=3)for(int edge=0;edge<3;edge++)\r\n {\r\n var a=mesh.Vertices[mesh.Triangles[t+edge]];var b=mesh.Vertices[mesh.Triangles[t+(edge+1)%3]];\r\n if(!((a.Y<=y&&b.Y>y)||(b.Y<=y&&a.Y>y)))continue;\r\n var p=Vector3.Lerp(a,b,(y-a.Y)/(b.Y-a.Y));\r\n minX=Math.Min(minX,p.X);maxX=Math.Max(maxX,p.X);minZ=Math.Min(minZ,p.Z);maxZ=Math.Max(maxZ,p.Z);count++;\r\n }\r\n if(count>=4&&maxX-minX>height*.005f&&maxZ-minZ>height*.005f)sections.Add(new(y,maxX-minX,maxZ-minZ));\r\n }\r\n var candidates=sections.Where(s=>s.Y<bottom+height*.9f).ToArray();\r\n if(candidates.Length==0)return height;\r\n var narrowest=candidates.MinBy(s=>s.Area);\r\n if(narrowest.Y>=bottom+height*.8f)return height;\r\n int index=sections.IndexOf(narrowest),first=index,last=index;\r\n bool SameNeck(Section a,Section b)=>b.Area<=narrowest.Area*2.5f&&Math.Abs(a.Y-b.Y)<=height*.0071f;\r\n while(first>0&&SameNeck(sections[first],sections[first-1]))first--;\r\n while(last+1<sections.Count&&SameNeck(sections[last],sections[last+1]))last++;\r\n if(last-first<2)return height;\r\n float neckY=(sections[first].Y+sections[last].Y)*.5f;\r\n // An unusually narrow waist is not the neck if another bottleneck\r\n // separates the shoulders and skull farther up the same silhouette.\r\n foreach(var later in sections.Where(s=>s.Y>neckY+height*.1f&&s.Y<bottom+height*.9f))\r\n {\r\n bool Expanded(Section s)=>s.Width>later.Width*1.35f&&s.Depth>later.Depth*1.1f;\r\n if(sections.Any(s=>s.Y<later.Y-height*.02f&&s.Y>later.Y-height*.07f&&Expanded(s))&&\r\n sections.Any(s=>s.Y>later.Y+height*.02f&&s.Y<later.Y+height*.07f&&Expanded(s)))return height;\r\n }\r\n // Require expansion in both transverse dimensions above the neck. An arm\r\n // silhouette or a narrow waist alone is not sufficient evidence of a head.\r\n if(!sections.Any(s=>s.Y>Math.Max(neckY+height*.025f,bottom+height*.82f)&&s.Width>narrowest.Width*2.2f&&s.Depth>narrowest.Depth*1.5f))return height;\r\n return Math.Min(height,(neckY-bottom)/.85f);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/HipFitting.cs",
"FileName": "HipFitting.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "#nullable enable annotations\r\nnamespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Refine a high hip from the thigh centerline and its junction with\r\n/// the pelvis. Ambiguous branches and large changes retain the original prior.</summary>\r\ninternal static class HipFitting\r\n{\r\n public static void RaiseLowHips(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)\r\n {\r\n var old=new[]{anatomy.Points[\"UpperLeg.L\"],anatomy.Points[\"UpperLeg.R\"]};\r\n if(old.Any(p=>p.Corrected))return;\r\n var candidates=new Vector3[2];float center=anatomy.SymmetryPlaneX;\r\n for(int i=0;i<2;i++)\r\n {\r\n float sign=i==0?1:-1;var hip=old[i].Position;\r\n var leg=sections.Where(s=>(s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.06f&&\r\n s.Center.Y>hip.Y-height*.07f&&s.Center.Y<hip.Y+height*.15f&&s.Radius<height*.095f).ToArray();\r\n var top=leg.MaxBy(s=>s.Center.Y);\r\n if(top is null||top.Center.Y<hip.Y+height*.015f||top.MinimumRadius<height*.012f)return;\r\n // Two separate thigh contours must actually join a central pelvis.\r\n if(!sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y>top.Center.Y&&s.Center.Y<top.Center.Y+height*.02f&&s.Area>top.Area*1.5f))return;\r\n // A torso contour already overlapping the thigh is a separate shell,\r\n // not evidence of a groin transition. Keep its anatomical prior.\r\n if(sections.Any(s=>Math.Abs(s.Center.X-center)<height*.015f&&s.Center.Y<top.Center.Y&&s.Center.Y>top.Center.Y-height*.03f&&s.Area>top.Area*1.5f))return;\r\n var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.015f&&s.Center.Y>top.Center.Y-height*.06f).ToArray();if(shaft.Length<4)return;\r\n // A narrowing end cap belongs to a detached leg segment. Extending\r\n // its radius would place the socket beyond its authored articulation.\r\n if(top.MinimumRadius<shaft.Average(s=>s.MinimumRadius)*.85f)return;\r\n var mean=Geometry.Mean(shaft.Select(s=>s.Center));float variance=shaft.Sum(s=>(s.Center.Y-mean.Y)*(s.Center.Y-mean.Y));if(variance<height*height*1e-8f)return;\r\n var slope=shaft.Aggregate(Vector3.Zero,(sum,s)=>sum+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;\r\n var candidate=mean+slope*(top.Center.Y+top.MinimumRadius-mean.Y);\r\n if(Vector3.Distance(candidate,hip)>height*.14f||!volume.Contains(candidate,height*.00001f))return;\r\n candidates[i]=candidate;\r\n }\r\n if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(old[0].Position.Y-old[1].Position.Y)+height*.01f)return;\r\n var pelvis=anatomy.Points[\"Pelvis\"];float lift=(candidates[0].Y+candidates[1].Y-old[0].Position.Y-old[1].Position.Y)*.5f;\r\n var moved=pelvis.Position+Vector3.UnitY*lift;\r\n if(!pelvis.Corrected&&volume.Contains(moved,height*.00001f))anatomy.Points[\"Pelvis\"]=pelvis with{Position=moved};\r\n for(int i=0;i<2;i++)anatomy.Points[old[i].Role]=old[i] with{Position=candidates[i]};\r\n }\r\n public static void Refine(Anatomy anatomy,IReadOnlyList<MeshSections.Section> sections,float height,SurfaceVisibility volume)\r\n {\r\n float center=anatomy.SymmetryPlaneX;\r\n var original=new[]{anatomy.Points[\"UpperLeg.L\"],anatomy.Points[\"UpperLeg.R\"]};\r\n var candidates=original.Select(p=>p.Position).ToArray();\r\n for(int index=0;index<2;index++)\r\n {\r\n string side=index==0?\"L\":\"R\";float sign=index==0?1:-1;\r\n var old=original[index];if(old.Corrected)continue;\r\n var hip=old.Position;var knee=anatomy[\"LowerLeg.\"+side];\r\n var leg=sections.Where(s=>s.Center.Y>knee.Y+(hip.Y-knee.Y)*.35f&&s.Center.Y<hip.Y+height*.01f&&\r\n (s.Center.X-center)*sign>height*.02f&&Math.Abs(s.Center.X-hip.X)<height*.07f&&s.Radius<height*.1f).ToArray();\r\n var top=leg.MaxBy(s=>s.Center.Y);\r\n if(top is null||top.Center.Y>hip.Y-height*.025f||top.MinimumRadius<height*.012f)continue;\r\n // The final contour is distorted by the groin. Fit the shaft below\r\n // that transition, rather than extending the pinched contour center.\r\n var shaft=leg.Where(s=>s.Center.Y<top.Center.Y-height*.02f&&s.Center.Y>top.Center.Y-height*.08f).ToArray();\r\n if(shaft.Length<4)continue;\r\n var mean=Geometry.Mean(shaft.Select(s=>s.Center));\r\n float variance=shaft.Sum(s=>MathF.Pow(s.Center.Y-mean.Y,2));if(variance<height*height*1e-8f)continue;\r\n var slope=shaft.Aggregate(Vector3.Zero,(value,s)=>value+(s.Center-mean)*(s.Center.Y-mean.Y))/variance;\r\n // A local inscribed radius locates the socket above the last\r\n // separated leg section, independently of total body proportions.\r\n float y=top.Center.Y+top.MinimumRadius;var candidate=mean+slope*(y-mean.Y);\r\n if(!Geometry.Finite(candidate)||candidate.Y>=hip.Y||(candidate.X-center)*sign<height*.01f||\r\n Vector3.Distance(candidate,hip)>Vector3.Distance(hip,knee)*.2f)continue;\r\n if(!Enumerable.Range(0,21).All(i=>volume.Contains(Vector3.Lerp(knee,candidate,i/20f),height*.00001f)))continue;\r\n candidates[index]=candidate;\r\n }\r\n // An isolated contour estimate cannot justify tilting the pelvis. Keep\r\n // existing asymmetry, but reject a new height mismatch larger than the\r\n // section sampling interval when the opposite socket lacks support.\r\n if(Math.Abs(candidates[0].Y-candidates[1].Y)>Math.Abs(original[0].Position.Y-original[1].Position.Y)+height*.005f)return;\r\n for(int i=0;i<2;i++)anatomy.Points[original[i].Role]=original[i] with{Position=candidates[i]};\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Export/VmdlBoneNames.cs",
"FileName": "VmdlBoneNames.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\n\r\n/// <summary>ModelDoc replaces namespace colons with underscores when importing bones.</summary>\r\npublic static class VmdlBoneNames\r\n{\r\n public static string Convert(string name)=>name.Replace(':','_');\r\n\r\n public static void Validate(IEnumerable<RigBone> bones)\r\n {\r\n var names=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);\r\n foreach(var bone in bones)\r\n {\r\n string converted=Convert(bone.Name);\r\n if(names.TryGetValue(converted,out var previous))\r\n throw new InvalidOperationException($\"Bone names '{previous}' and '{bone.Name}' both become '{converted}' in s&box. Rename one bone in the profile.\");\r\n names.Add(converted,bone.Name);\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/Inference/HandModelAssets.cs",
"FileName": "HandModelAssets.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "using System.IO.Compression;\r\nusing System.Net.Http;\r\nusing System.Security.Cryptography;\r\nusing System.Threading.Tasks;\r\nnamespace HumanoidRigger;\r\n\r\n/// <summary>Pinned, hash-checked model/runtime cache. Downloads never contain user model data.</summary>\r\npublic static class HandModelAssets\r\n{\r\n public const string ModelHash=\"db0898ae717b76b075d9bf563af315b29562e11f8df5027a1ef07b02bef6d81c\";\r\n public const string RuntimeHash=\"dec964ab1ee36cc9b0ae247d13b376627992fc57dec0454354017ab8fd84f1ea\";\r\n public static string CacheDirectory=>Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),\"sbox-humanoid-rigger\",\"inference\",\"hand-v1\");\r\n public static async Task<NativeHandModel> Load(string folder)\r\n {\r\n Directory.CreateDirectory(folder);\r\n string model=Path.Combine(folder,\"hand.onnx\"),runtime=Path.Combine(folder,\"onnxruntime.dll\");\r\n using var http=new HttpClient{Timeout=TimeSpan.FromSeconds(90)};\r\n await Download(http,\"https://media.githubusercontent.com/media/opencv/opencv_zoo/25f423d0e04c31a17254620e58febd7386da523b/models/handpose_estimation_mediapipe/handpose_estimation_mediapipe_2023feb.onnx\",model,ModelHash);\r\n if(!Valid(runtime,RuntimeHash))\r\n {\r\n string archive=Path.Combine(folder,\"onnxruntime-1.23.2.zip\");\r\n await Download(http,\"https://github.com/microsoft/onnxruntime/releases/download/v1.23.2/onnxruntime-win-x64-1.23.2.zip\",archive,\"0b38df9af21834e41e73d602d90db5cb06dbd1ca618948b8f1d66d607ac9f3cd\");\r\n using var zip=ZipFile.OpenRead(archive);\r\n foreach(var name in new[]{\"lib/onnxruntime.dll\",\"LICENSE\",\"ThirdPartyNotices.txt\"})\r\n {\r\n var entry=zip.GetEntry(\"onnxruntime-win-x64-1.23.2/\"+name)??throw new InvalidDataException(\"Missing runtime asset.\");\r\n string destination=Path.Combine(folder,Path.GetFileName(name)),temporary=destination+\".\"+Guid.NewGuid().ToString(\"N\")+\".tmp\";\r\n try{entry.ExtractToFile(temporary);if(name.EndsWith(\".dll\")&&!Valid(temporary,RuntimeHash))throw new InvalidDataException(\"Runtime checksum mismatch.\");File.Move(temporary,destination,true);}\r\n finally{if(File.Exists(temporary))File.Delete(temporary);}\r\n }\r\n }\r\n if(!Valid(runtime,RuntimeHash)||!Valid(model,ModelHash))throw new InvalidDataException(\"Hand inference asset checksum mismatch.\");\r\n return new NativeHandModel(runtime,model);\r\n }\r\n public static bool Valid(string path,string hash)\r\n {\r\n if(!File.Exists(path))return false;\r\n using var stream=File.OpenRead(path);return Convert.ToHexString(SHA256.HashData(stream)).Equals(hash,StringComparison.OrdinalIgnoreCase);\r\n }\r\n static async Task Download(HttpClient http,string url,string path,string hash)\r\n {\r\n if(Valid(path,hash))return;\r\n string temporary=path+\".\"+Guid.NewGuid().ToString(\"N\")+\".tmp\";\r\n try\r\n {\r\n using var response=await http.GetAsync(url,HttpCompletionOption.ResponseHeadersRead);response.EnsureSuccessStatusCode();\r\n using(var destination=File.Create(temporary))await response.Content.CopyToAsync(destination);\r\n if(!Valid(temporary,hash))throw new InvalidDataException(\"Hand inference download checksum mismatch.\");\r\n File.Move(temporary,path,true);\r\n }\r\n finally{if(File.Exists(temporary))File.Delete(temporary);}\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Import/Fbx/FbxNode.cs",
"FileName": "FbxNode.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "#nullable enable annotations\r\n\r\nnamespace HumanoidRigger.Formats.Fbx;\r\n\r\n/// <summary>\r\n/// A single node of an FBX document tree (binary or ASCII): a name, a flat list of\r\n/// typed properties, and nested child nodes.\r\n///\r\n/// Property values are stored as the closest CLR type to what the file contained:\r\n/// <list type=\"bullet\">\r\n/// <item><c>short</c> ('Y'), <c>bool</c> ('C'), <c>int</c> ('I'), <c>float</c> ('F'),\r\n/// <c>double</c> ('D'), <c>long</c> ('L')</item>\r\n/// <item><c>float[]</c> ('f'), <c>double[]</c> ('d'), <c>long[]</c> ('l'),\r\n/// <c>int[]</c> ('i'), <c>bool[]</c>-as-<c>byte[]</c> ('b')</item>\r\n/// <item><c>string</c> ('S' \u2014 kept raw, may contain the <c>\\x00\\x01</c> name/class\r\n/// separator; see <see cref=\"SplitName\"/>), <c>byte[]</c> ('R')</item>\r\n/// </list>\r\n/// ASCII files store numbers only as <c>long</c> / <c>double</c> (and arrays as\r\n/// <c>long[]</c> / <c>double[]</c>), so the typed accessors below convert tolerantly.\r\n/// </summary>\r\npublic sealed class FbxNode\r\n{\r\n public string Name { get; }\r\n public List<object> Properties { get; } = new();\r\n public List<FbxNode> Children { get; } = new();\r\n\r\n public FbxNode(string name) => Name = name;\r\n\r\n /// <summary>First child with the given name, or null.</summary>\r\n public FbxNode? Child(string name)\r\n {\r\n foreach (var c in Children)\r\n if (c.Name == name)\r\n return c;\r\n return null;\r\n }\r\n\r\n /// <summary>All children with the given name, in document order.</summary>\r\n public IEnumerable<FbxNode> ChildrenNamed(string name)\r\n {\r\n foreach (var c in Children)\r\n if (c.Name == name)\r\n yield return c;\r\n }\r\n\r\n /// <summary>\r\n /// Property <paramref name=\"i\"/> converted to <typeparamref name=\"T\"/>.\r\n /// Numeric scalars convert tolerantly across widths (e.g. an 'I' i32 read as long);\r\n /// anything else must match the stored type exactly.\r\n /// </summary>\r\n public T Prop<T>(int i)\r\n {\r\n object v = RawProp(i);\r\n if (v is T t)\r\n return t;\r\n\r\n var target = typeof(T);\r\n // s&box whitelist: Type.IsPrimitive is banned; enumerate the convertible targets.\r\n if (v is IConvertible && (ConvertTargets.Contains(target) || target == typeof(string)))\r\n {\r\n try\r\n {\r\n return (T)Convert.ChangeType(v, target, System.Globalization.CultureInfo.InvariantCulture);\r\n }\r\n // ArithmeticException covers OverflowException, which is not s&box-whitelisted\r\n catch (Exception ex) when (ex is InvalidCastException or ArithmeticException or FormatException)\r\n {\r\n throw new FormatException(\r\n $\"FBX node '{Name}': property {i} is {v.GetType().Name}, not convertible to {target.Name}.\", ex);\r\n }\r\n }\r\n\r\n throw new FormatException(\r\n $\"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {target.Name}.\");\r\n }\r\n\r\n /// <summary>Property <paramref name=\"i\"/> as a double array (converts f/l/i/b arrays).</summary>\r\n public double[] AsDoubleArray(int i) => RawProp(i) switch\r\n {\r\n double[] d => d,\r\n float[] f => Array.ConvertAll(f, x => (double)x),\r\n long[] l => Array.ConvertAll(l, x => (double)x),\r\n int[] n => Array.ConvertAll(n, x => (double)x),\r\n byte[] b => Array.ConvertAll(b, x => (double)x),\r\n bool[] o => Array.ConvertAll(o, x => x ? 1.0 : 0.0),\r\n var v => throw TypeError(i, v, \"double[]\"),\r\n };\r\n\r\n /// <summary>Property <paramref name=\"i\"/> as a float array (converts d/l/i/b arrays).</summary>\r\n public float[] AsFloatArray(int i) => RawProp(i) switch\r\n {\r\n float[] f => f,\r\n double[] d => Array.ConvertAll(d, x => (float)x),\r\n long[] l => Array.ConvertAll(l, x => (float)x),\r\n int[] n => Array.ConvertAll(n, x => (float)x),\r\n byte[] b => Array.ConvertAll(b, x => (float)x),\r\n bool[] o => Array.ConvertAll(o, x => x ? 1f : 0f),\r\n var v => throw TypeError(i, v, \"float[]\"),\r\n };\r\n\r\n /// <summary>Property <paramref name=\"i\"/> as a long array (converts i/b; d/f if integral).</summary>\r\n public long[] AsLongArray(int i) => RawProp(i) switch\r\n {\r\n long[] l => l,\r\n int[] n => Array.ConvertAll(n, x => (long)x),\r\n byte[] b => Array.ConvertAll(b, x => (long)x),\r\n bool[] o => Array.ConvertAll(o, x => x ? 1L : 0L),\r\n double[] d => Array.ConvertAll(d, x => checked((long)x)),\r\n float[] f => Array.ConvertAll(f, x => checked((long)x)),\r\n var v => throw TypeError(i, v, \"long[]\"),\r\n };\r\n\r\n /// <summary>Property <paramref name=\"i\"/> as an int array (converts b; l/d/f narrowing-checked).</summary>\r\n public int[] AsIntArray(int i) => RawProp(i) switch\r\n {\r\n int[] n => n,\r\n long[] l => Array.ConvertAll(l, x => checked((int)x)),\r\n byte[] b => Array.ConvertAll(b, x => (int)x),\r\n bool[] o => Array.ConvertAll(o, x => x ? 1 : 0),\r\n double[] d => Array.ConvertAll(d, x => checked((int)x)),\r\n float[] f => Array.ConvertAll(f, x => checked((int)x)),\r\n var v => throw TypeError(i, v, \"int[]\"),\r\n };\r\n\r\n /// <summary>Property <paramref name=\"i\"/> as raw bytes ('R' blobs or 'b' bool arrays).</summary>\r\n public byte[] AsByteArray(int i) => RawProp(i) switch\r\n {\r\n byte[] b => b,\r\n var v => throw TypeError(i, v, \"byte[]\"),\r\n };\r\n\r\n /// <summary>Property <paramref name=\"i\"/> as a string (raw 'S' content, separators intact).</summary>\r\n public string AsString(int i) => RawProp(i) switch\r\n {\r\n string s => s,\r\n var v => throw TypeError(i, v, \"string\"),\r\n };\r\n\r\n /// <summary>\r\n /// Splits an FBX object name into (name, class).\r\n /// Binary files store <c>\"Name\\x00\\x01Class\"</c> (e.g. <c>\"mixamorig:Hips\\x00\\x01Model\"</c>);\r\n /// ASCII files store <c>\"Class::Name\"</c> (e.g. <c>\"Model::pelvis\"</c>).\r\n /// A plain string with neither separator yields (name, \"\").\r\n /// </summary>\r\n public static (string Name, string Class) SplitName(string raw)\r\n {\r\n int bin = raw.IndexOf(\"\\0\\x01\", StringComparison.Ordinal);\r\n if (bin >= 0)\r\n return (raw[..bin], raw[(bin + 2)..]);\r\n\r\n int ascii = raw.IndexOf(\"::\", StringComparison.Ordinal);\r\n if (ascii >= 0)\r\n return (raw[(ascii + 2)..], raw[..ascii]);\r\n\r\n return (raw, \"\");\r\n }\r\n\r\n /// <summary>Primitive scalar types <see cref=\"Prop{T}\"/> converts to (whitelist-safe IsPrimitive substitute).</summary>\r\n private static readonly HashSet<Type> ConvertTargets = new()\r\n {\r\n typeof(bool), typeof(byte), typeof(sbyte), typeof(short), typeof(ushort),\r\n typeof(int), typeof(uint), typeof(long), typeof(ulong),\r\n typeof(float), typeof(double), typeof(char),\r\n };\r\n\r\n private object RawProp(int i)\r\n {\r\n if (i < 0 || i >= Properties.Count)\r\n throw new FormatException(\r\n $\"FBX node '{Name}': property index {i} out of range (has {Properties.Count}).\");\r\n return Properties[i];\r\n }\r\n\r\n private FormatException TypeError(int i, object v, string wanted) =>\r\n new($\"FBX node '{Name}': property {i} is {v.GetType().Name}, expected {wanted}.\");\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Validation/JointWeightRepair.cs",
"FileName": "JointWeightRepair.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "#nullable enable annotations\r\nnamespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Regularize a limb's blend with its parent using the measured joint\r\n/// thickness. Trials retain connectivity and are accepted after complete stress\r\n/// testing and local repair; reviewed bones and source geometry never move.</summary>\r\ninternal static class JointWeightRepair\r\n{\r\n internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig,ValidationGeometry? geometry=null)\r\n {\r\n var roles=rig.Bones.Select(b=>b.Role).ToHashSet();\r\n var specifications=(geometry?.Poses??Deformation.Poses).Where(p=>Deformation.IsApplicable(p,roles)).ToArray();\r\n var expected=specifications.Select(p=>p.Name).Order().ToArray();\r\n if(!WeightRepair.HasCompleteEvidence(rig.Report,expected)||rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return rig;\r\n geometry??=new ValidationGeometry(character);var faces=geometry.Faces;\r\n var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();\r\n float height=character.AnatomicalHeight;\r\n for(int pass=0;pass<4;pass++)\r\n {\r\n int before=rig.Report.StressTests.Sum(p=>p.ReversedTriangles);\r\n foreach(var scheduled in rig.Report.StressTests.Where(p=>p.ReversedTriangles>0)\r\n .OrderByDescending(p=>p.MaximumStretch>4||p.MinimumAreaRatio<.025f).ThenByDescending(p=>p.ReversedAreaFraction).ToArray())\r\n {\r\n var stress=rig.Report.StressTests.Single(p=>p.Pose==scheduled.Pose);\r\n if(stress.ReversedTriangles==0)continue;\r\n var specification=specifications.Single(p=>p.Name==stress.Pose);\r\n if(geometry.Poses.Count==Deformation.Poses.Count&&!new[]{\"UpperArm.\",\"LowerArm.\",\"UpperLeg.\",\"LowerLeg.\"}.Any(specification.Role.StartsWith))continue;\r\n int joint=Array.FindIndex(rig.Bones,b=>b.Role==specification.Role);var bone=rig.Bones[joint];\r\n var child=rig.Bones.FirstOrDefault(b=>b.Parent==joint&&b.Deform);\r\n if(child is null||bone.Parent<0||!rig.Bones[bone.Parent].Deform||Vector3.DistanceSquared(child.Position,bone.Position)<1e-8f)continue;\r\n var axis=Vector3.Normalize(child.Position-bone.Position);var moving=new bool[rig.Bones.Length];moving[joint]=true;\r\n for(int i=joint+1;i<moving.Length;i++)moving[i]=rig.Bones[i].Parent>=0&&moving[rig.Bones[i].Parent];\r\n float sum=0;int count=0;\r\n foreach(var mesh in character.Meshes)foreach(var p in mesh.Vertices)\r\n {\r\n var delta=p-bone.Position;float along=Vector3.Dot(delta,axis),radial=(delta-axis*along).Length();\r\n if(Math.Abs(along)<height*.006f&&radial<height*.07f){sum+=radial;count++;}\r\n }\r\n float radius=Math.Clamp(count>0?sum/count:height*.025f,height*.01f,height*.065f);\r\n var totals=rig.Weights.Select(part=>part.Select(weights=>MovingTotal(weights,moving)).ToArray()).ToArray();\r\n var axial=character.Meshes.Select(mesh=>mesh.Vertices.Select(point=>Vector3.Dot(point-bone.Position,axis)).ToArray()).ToArray();\r\n var trials=new List<(GeneratedRig Rig,StressResult Stress)>();\r\n var blends=stress.ReversedTriangles<16?new[]{-.025f,-.05f,-.1f,-.2f,-.35f,.025f,.05f,.1f,.2f,.35f}:new[]{.5f,1f};\r\n foreach(float width in new[]{2f,3f,4f,6f})foreach(float blend in blends)\r\n {\r\n var weights=character.Meshes.Select((mesh,part)=>mesh.Vertices.Select((point,vertex)=>\r\n Blend(rig.Weights[part][vertex],rig.Profile.MaximumInfluences,rig.Bones.Length,moving,bone.Parent,\r\n totals[part][vertex],axial[part][vertex]/(2*width*radius),blend)).ToArray()).ToArray();\r\n var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Weights=weights,Anatomy=rig.Anatomy};\r\n var result=RigValidator.MeasurePose(character,candidate,specification,faces,buffer,height);\r\n if(result.ReversedTriangles>=stress.ReversedTriangles||result.ReversedAreaFraction>stress.ReversedAreaFraction)continue;\r\n trials.Add((candidate,result));\r\n // Only the best three trials are tested below. Release other\r\n // dense weight buffers immediately, preserving stable tie order.\r\n if(trials.Count>3)trials=trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3).ToList();\r\n }\r\n // A promising broad correction can expose a local seam. Run the\r\n // normal cleanup and repair before deciding whether it is better.\r\n foreach(var trial in trials.OrderBy(t=>t.Stress.ReversedTriangles).ThenBy(t=>t.Stress.ReversedAreaFraction).Take(3))\r\n {\r\n var report=RigValidator.ValidateAndRepair(character,trial.Rig,geometry);\r\n if(!SkeletonSolver.BetterSkinning(report,rig.Report,expected))continue;\r\n report.Repairs+=rig.Report.Repairs;report.RepairPasses+=rig.Report.RepairPasses+1;\r\n trial.Rig.Report=report;rig=trial.Rig;break;\r\n }\r\n }\r\n if(before==rig.Report.StressTests.Sum(p=>p.ReversedTriangles))break;\r\n }\r\n return rig;\r\n }\r\n static float MovingTotal(Influence[] weights,bool[] moving)\r\n {\r\n double total=0;foreach(var w in weights)if(moving[w.Bone])total+=w.Weight;\r\n return(float)total;\r\n }\r\n static Influence[] Blend(Influence[] source,int maximum,int boneCount,bool[] moving,int parent,float total,float axial,float amount)\r\n {\r\n if(total<.0001f)return source;\r\n float t=Math.Clamp(.5f+axial,0,1),envelope=t*t*(3-2*t);\r\n if(amount<0)\r\n {\r\n if(total>.9999f)return source;\r\n float target=total+(-amount)*total*(1-total)*(1-envelope);\r\n var scaled=new Influence[source.Length];\r\n for(int i=0;i<source.Length;i++){var w=source[i];scaled[i]=w with{Weight=w.Weight*(moving[w.Bone]?target/total:(1-target)/(1-total))};}\r\n return Skinning.Cleanup(scaled,boneCount,maximum);\r\n }\r\n float retained=1-amount+envelope*amount;\r\n var blended=new Influence[source.Length+1];\r\n for(int i=0;i<source.Length;i++){var w=source[i];blended[i]=w with{Weight=w.Weight*(moving[w.Bone]?retained:1)};}\r\n blended[^1]=new(parent,total*(1-retained));\r\n return Skinning.Cleanup(blended,boneCount,maximum);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Validation/JointCoverage.cs",
"FileName": "JointCoverage.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Check every deforming joint in both directions on each axis.\r\n/// Only faces reached by that joint's skin weights need geometric measurement.</summary>\r\ninternal static class JointCoverage\r\n{\r\n internal static (StressPose Pose,StressResult Result)[] Measure(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)\r\n {\r\n var result=new List<(StressPose,StressResult)>();\r\n var buffer=character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();\r\n for(int joint=0;joint<rig.Bones.Length;joint++)\r\n {\r\n if(!rig.Bones[joint].Deform)continue;\r\n var moving=new bool[rig.Bones.Length];moving[joint]=true;\r\n for(int b=joint+1;b<moving.Length;b++)moving[b]=rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];\r\n var touched=rig.Weights.Select(p=>p.Select(w=>w.Any(i=>moving[i.Bone])).ToArray()).ToArray();\r\n var faces=geometry.Faces.Select((p,m)=>p.Where(f=>touched[m][f.A]||touched[m][f.B]||touched[m][f.C]).ToArray()).ToArray();\r\n foreach(var (axis,name) in new[]{(Vector3.UnitX,\"X\"),(Vector3.UnitY,\"Y\"),(Vector3.UnitZ,\"Z\")})foreach(float degrees in new[]{-30f,30f})\r\n {\r\n var pose=new StressPose($\"Joint {rig.Bones[joint].Role} {name} {degrees:+0;-0}\",rig.Bones[joint].Role,axis,degrees);\r\n result.Add((pose,RigValidator.MeasurePose(character,rig,pose,faces,buffer,geometry.Height)));\r\n }\r\n }\r\n return result.ToArray();\r\n }\r\n static bool Bad(StressResult result)=>result.ReversedTriangles>0||result.NonFiniteVertices>0||result.NonFiniteMeasurements>0||result.MaximumStretch>4||result.MinimumAreaRatio<.025f;\r\n static double Score(IEnumerable<StressResult> results)=>results.Sum(r=>r.ReversedTriangles+1000000d*(r.NonFiniteVertices+r.NonFiniteMeasurements)+100*Math.Max(0,r.MaximumStretch/4-1)+100*Math.Max(0,1-r.MinimumAreaRatio/.025f));\r\n internal static GeneratedRig Improve(ImportedCharacter character,GeneratedRig rig)\r\n {\r\n if(!rig.Report.Passed)return rig;\r\n var geometry=new ValidationGeometry(character);var checks=Measure(character,rig,geometry);\r\n if(checks.Any(c=>Bad(c.Result)))\r\n {\r\n var repaired=PoseWeightRepair.Improve(character,rig,geometry,checks);\r\n if(!ReferenceEquals(repaired,rig)){rig=repaired;checks=Measure(character,rig,geometry);}\r\n }\r\n var trunk=new TrunkRegion(character,rig);\r\n var normalHeat=new Lazy<Influence[][][]>(()=>HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:trunk).First());\r\n var heat=new Lazy<Influence[][][]>(()=>HeatSkinning.Solve(character,rig));\r\n var constraints=new Dictionary<string,StressPose>();\r\n for(int pass=0;pass<4&&checks.Any(c=>Bad(c.Result));pass++)\r\n {\r\n var failed=checks.Where(c=>Bad(c.Result)).OrderByDescending(c=>c.Result.ReversedTriangles).Take(24).Select(c=>c.Pose).ToArray();\r\n // Retain previously discovered failures after they are repaired.\r\n // Otherwise a spine correction can undo the adjacent chest repair.\r\n foreach(var pose in failed)constraints.TryAdd(pose.Name,pose);\r\n var scope=new ValidationGeometry(character,Deformation.Poses.Concat(constraints.Values).ToArray(),trunk);\r\n var candidate=new GeneratedRig{Profile=rig.Profile,Bones=rig.Bones,Anatomy=rig.Anatomy,Weights=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray()};\r\n candidate.Report=RigValidator.ValidateAndRepair(character,candidate,scope);\r\n candidate=JointWeightRepair.Improve(character,candidate,scope);\r\n if(candidate.Report.StressTests.Any(p=>p.ReversedTriangles>0))\r\n RefineSources(character,candidate,scope,failed,normalHeat,heat);\r\n if(!candidate.Report.Passed||trunk.HasBleeding(character,candidate))break;\r\n var standard=RigValidator.Validate(character,candidate);\r\n if(!standard.Passed||standard.StressTests.Zip(rig.Report.StressTests).Any(p=>p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))break;\r\n var next=Measure(character,candidate,geometry);\r\n bool discovered=false;\r\n foreach(var check in next.Where(c=>Bad(c.Result)))discovered|=constraints.TryAdd(check.Pose.Name,check.Pose);\r\n if(Score(next.Select(c=>c.Result))>=Score(checks.Select(c=>c.Result)))\r\n {if(discovered)continue;break;}\r\n standard.Repairs=rig.Report.Repairs+candidate.Report.Repairs;standard.RepairPasses=rig.Report.RepairPasses+candidate.Report.RepairPasses+1;\r\n candidate.Report=standard;rig=candidate;checks=next;\r\n }\r\n rig.Report.JointStressTests.AddRange(checks.Select(c=>c.Result));\r\n var remaining=checks.Where(c=>Bad(c.Result)).ToArray();\r\n if(remaining.Length>0)rig.Report.Issues.Add(new(\"joint-deformation\",$\"{remaining.Length} joint motions still have unsafe deformation: {string.Join(\", \",remaining.Select(c=>c.Pose.Role).Distinct())}.\",true));\r\n return rig;\r\n }\r\n static void RefineSources(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry,StressPose[] failed,params Lazy<Influence[][][]>[] fields)\r\n {\r\n var roots=failed.Select(p=>p.Role).ToHashSet();var moving=new bool[rig.Bones.Length];\r\n for(int b=0;b<moving.Length;b++)moving[b]=roots.Contains(rig.Bones[b].Role)||rig.Bones[b].Parent>=0&&moving[rig.Bones[b].Parent];\r\n foreach(var field in fields)\r\n {\r\n Influence[][][] source;\r\n try{source=field.Value;}catch(InvalidOperationException){continue;}\r\n foreach(float amount in new[]{1f,.5f,.25f})\r\n {\r\n var proposed=rig.Weights.Select(p=>(Influence[][])p.Clone()).ToArray();\r\n for(int p=0;p<proposed.Length;p++)for(int v=0;v<proposed[p].Length;v++)\r\n {\r\n float support=Math.Min(1,rig.Weights[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight)+source[p][v].Where(w=>moving[w.Bone]).Sum(w=>w.Weight))*amount;\r\n if(support<.001f)continue;\r\n var weights=Skinning.Cleanup(rig.Weights[p][v].Select(w=>w with{Weight=w.Weight*(1-support)})\r\n .Concat(source[p][v].Select(w=>w with{Weight=w.Weight*support})),rig.Bones.Length,rig.Profile.MaximumInfluences);\r\n if(geometry.Trunk?.Allows(p,v,character.Meshes[p].Vertices[v],weights)==false)continue;\r\n proposed[p][v]=weights;\r\n }\r\n rig.Report=SurfaceRepair.TryWeights(character,rig,rig.Report,proposed,geometry);\r\n rig.Report=SurfaceRepair.Improve(character,rig,rig.Report,geometry);\r\n if(rig.Report.StressTests.All(p=>p.ReversedTriangles==0))return;\r\n }\r\n }\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Usings.cs",
"FileName": "Usings.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "global using System;\r\nglobal using System.Collections.Generic;\r\nglobal using System.Linq;\r\nglobal using System.IO;\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/UI/MaterialAssets.Sidecars.cs",
"FileName": "MaterialAssets.Sidecars.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "// Copied from humanoid-retargeter Editor/HumanoidRetargeter/EditorPipeline.cs.\r\nusing Editor;\r\nusing Sandbox;\r\nnamespace HumanoidRigger.Editor;\r\ninternal static partial class MaterialAssets\r\n{\r\n\tstatic readonly string[] TextureExtensions =\r\n\t\t{ \".png\", \".jpg\", \".jpeg\", \".tga\", \".dds\", \".webp\", \".vmat\", \".vtex\" };\r\n\r\n\t/// <summary>Copies texture sidecars of a picked target model into the output folder:\r\n\t/// loose image files next to it, and a \"textures\" folder next to it or next to its\r\n\t/// parent (the source/-plus-textures/ layout). Per-file best effort - a failed texture\r\n\t/// must never fail the conversion.</summary>\r\n\tstatic void CopySidecarTextures( string sourceDir, string destDir )\r\n\t{\r\n\t\ttry\r\n\t\t{\r\n\t\t\tif ( sourceDir is null || destDir is null )\r\n\t\t\t\treturn;\r\n\t\t\tsourceDir = Path.GetFullPath( sourceDir );\r\n\t\t\tdestDir = Path.GetFullPath( destDir );\r\n\t\t\tif ( string.Equals( sourceDir, destDir, StringComparison.OrdinalIgnoreCase ) )\r\n\t\t\t\treturn;\r\n\r\n\t\t\t// Every copied file must be REGISTERED: assets copied onto disk mid-session are\r\n\t\t\t// unknown to the asset system, so the material chain cannot generate their vtex\r\n\t\t\t// resources - the renderer then logs \"Texture manager doesn't know about\r\n\t\t\t// texture ...generated.vtex\" MANY TIMES PER FRAME, which is both the\r\n\t\t\t// purple/black flicker and a preview running at ~2 fps (user report).\r\n\t\t\tforeach ( var file in Directory.GetFiles( sourceDir ) )\r\n\t\t\t{\r\n\t\t\t\tif ( !TextureExtensions.Contains( Path.GetExtension( file ).ToLowerInvariant() ) )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\tvar destFile = Path.Combine( destDir, Path.GetFileName( file ) );\r\n\t\t\t\tTry( () => { File.Copy( file, destFile, true ); return true; } );\r\n\t\t\t\tTry( () => AssetSystem.RegisterFile( destFile ) );\r\n\t\t\t}\r\n\r\n\t\t\tforeach ( var candidate in new[]\r\n\t\t\t{\r\n\t\t\t\tPath.Combine( sourceDir, \"textures\" ),\r\n\t\t\t\tPath.Combine( Path.GetDirectoryName( sourceDir ) ?? sourceDir, \"textures\" ),\r\n\t\t\t} )\r\n\t\t\t{\r\n\t\t\t\tif ( !Directory.Exists( candidate ) )\r\n\t\t\t\t\tcontinue;\r\n\t\t\t\tvar destTextures = Path.Combine( destDir, \"textures\" );\r\n\t\t\t\tDirectory.CreateDirectory( destTextures );\r\n\t\t\t\tforeach ( var file in Directory.GetFiles( candidate, \"*\", SearchOption.AllDirectories ) )\r\n\t\t\t\t{\r\n\t\t\t\t\tvar relative = Path.GetRelativePath( candidate, file );\r\n\t\t\t\t\tvar destFile = Path.Combine( destTextures, relative );\r\n\t\t\t\t\tTry( () =>\r\n\t\t\t\t\t{\r\n\t\t\t\t\t\tDirectory.CreateDirectory( Path.GetDirectoryName( destFile ) );\r\n\t\t\t\t\t\tFile.Copy( file, destFile, true );\r\n\t\t\t\t\t\treturn true;\r\n\t\t\t\t\t} );\r\n\t\t\t\t\tTry( () => AssetSystem.RegisterFile( destFile ) );\r\n\t\t\t\t}\r\n\t\t\t\tbreak; // first existing candidate wins\r\n\t\t\t}\r\n\t\t}\r\n\t\tcatch ( Exception e )\r\n\t\t{\r\n\t\t\tLog.Warning( $\"[sbox-humanoid-rigger] sidecar texture copy failed: {e.Message}\" );\r\n\t\t}\r\n\t}\r\n\r\n\r\nstatic T Try<T>(Func<T> action){try{return action();}catch(Exception e){Log.Warning(e.Message);return default;}}\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/UI/StatusChip.cs",
"FileName": "StatusChip.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "using Editor;\r\nusing Sandbox;\r\n\r\nnamespace HumanoidRigger.Editor;\r\n\r\n/// <summary>Copied from Humanoid Retargeter's RetargetWindow.Chip.\r\n/// Keep its dimensions, typography, fill and radius aligned with the suite.</summary>\r\nsealed class StatusChip : Widget\r\n{\r\n readonly string text;\r\n readonly Color color;\r\n\r\n public StatusChip(Widget parent,string text,Color color) : base(parent)\r\n {\r\n this.text=text;\r\n this.color=color;\r\n FixedHeight=20;\r\n FixedWidth=7.2f*text.Length+18;\r\n }\r\n\r\n protected override void OnPaint()\r\n {\r\n Paint.ClearPen();\r\n Paint.SetBrush(color.WithAlpha(.18f));\r\n Paint.DrawRect(LocalRect,LocalRect.Height*.5f);\r\n Paint.SetPen(color);\r\n Paint.SetDefaultFont(7,600);\r\n Paint.DrawText(LocalRect,text);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/HumanoidFacing.cs",
"FileName": "HumanoidFacing.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Resolve up from body geometry, then an unambiguous quarter-turn\r\n/// from separated lower legs. Feet select the forward sign.</summary>\r\ninternal static class HumanoidFacing\r\n{\r\n public static ImportedCharacter Normalize(ImportedCharacter character)\r\n {\r\n var up=HumanoidUp.Find(character);\r\n if(up!=Vector3.UnitY)\r\n {\r\n Vector3 Upright(Vector3 p)=>up==Vector3.UnitX?new(-p.Y,p.X,p.Z)\r\n :up==-Vector3.UnitX?new(p.Y,-p.X,p.Z)\r\n :up==-Vector3.UnitY?new(p.X,-p.Y,-p.Z)\r\n :up==Vector3.UnitZ?new(p.X,p.Z,-p.Y):new(p.X,-p.Z,p.Y);\r\n character=Reorient(character,Upright,\"The character's up direction was corrected from its body geometry.\");\r\n }\r\n var body=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();\r\n if(body.Length<100)return character;\r\n float bottom=body.Min(p=>p.Y),height=body.Max(p=>p.Y)-bottom;\r\n var legs=body.Where(p=>p.Y>bottom+height*.15f&&p.Y<bottom+height*.35f).Distinct().ToArray();\r\n if(legs.Length<20)return character;\r\n var center=Geometry.Mean(legs);\r\n float xVariance=legs.Average(p=>(p.X-center.X)*(p.X-center.X));\r\n float zVariance=legs.Average(p=>(p.Z-center.Z)*(p.Z-center.Z));\r\n if(zVariance<xVariance*3)return character;\r\n float middle=(BodyDetector.Quantile(legs.Select(p=>p.Z),.1f)+BodyDetector.Quantile(legs.Select(p=>p.Z),.9f))*.5f;\r\n if(legs.Count(p=>Math.Abs(p.Z-middle)<height*.012f)>legs.Length*.15f)return character;\r\n var feet=body.Where(p=>p.Y<bottom+height*.07f).ToArray();\r\n if(feet.Length<8)return character;\r\n float forward=(BodyDetector.Quantile(feet.Select(p=>p.X),.1f)+BodyDetector.Quantile(feet.Select(p=>p.X),.9f))*.5f-center.X;\r\n forward=SeparatedFeetForward(character,bottom,height,middle)??forward;\r\n if(Math.Abs(forward)<height*.012f)return character;\r\n float sign=forward<0?1:-1;\r\n Vector3 Turn(Vector3 p)=>new(sign*p.Z,p.Y,-sign*p.X);\r\n return Reorient(character,Turn,\"The character was turned to face forward.\");\r\n }\r\n static float? SeparatedFeetForward(ImportedCharacter character,float bottom,float height,float middle)\r\n {\r\n // Long hands can reach the floor. If the lower slice contains more than\r\n // two limbs, follow the inner calf surfaces down to their own feet.\r\n var origin=(character.Minimum+character.Maximum)*.5f;origin.Y=bottom+height*.25f;\r\n var sections=MeshSections.Cut(character,origin,Vector3.UnitY,(character.Maximum-character.Minimum).Length(),height*1e-5f);\r\n if(sections.Length<=2)return null;\r\n var left=sections.Where(s=>s.Center.Z>middle+height*.025f).OrderBy(s=>s.Center.Z).FirstOrDefault();\r\n var right=sections.Where(s=>s.Center.Z<middle-height*.025f).OrderByDescending(s=>s.Center.Z).FirstOrDefault();\r\n if(left is null||right is null)return null;\r\n var mesh=Geometry.Merge(character.Meshes.Where(m=>m.Kind==MeshKind.Body));\r\n var neighbors=Geometry.Neighbors(mesh,height*1e-5f);var visited=new bool[mesh.Vertices.Length];var queue=new Queue<int>();\r\n foreach(var section in new[]{left,right})\r\n {\r\n int seed=-1;float distance=float.PositiveInfinity;\r\n for(int i=0;i<mesh.Vertices.Length;i++)\r\n {\r\n float candidate=Vector3.DistanceSquared(mesh.Vertices[i],section.Center);\r\n if(candidate<distance){seed=i;distance=candidate;}\r\n }\r\n if(seed<0||distance>height*height*.08f*.08f)return null;\r\n if(!visited[seed]){visited[seed]=true;queue.Enqueue(seed);}\r\n }\r\n while(queue.TryDequeue(out int vertex))foreach(int next in neighbors[vertex])\r\n if(!visited[next]&&mesh.Vertices[next].Y<bottom+height*.4f){visited[next]=true;queue.Enqueue(next);}\r\n var feet=mesh.Vertices.Where((p,i)=>visited[i]&&p.Y<bottom+height*.07f).Select(p=>p.X).ToArray();\r\n if(feet.Length<8)return null;\r\n return(BodyDetector.Quantile(feet,.1f)+BodyDetector.Quantile(feet,.9f))*.5f-(left.Center.X+right.Center.X)*.5f;\r\n }\r\n static ImportedCharacter Reorient(ImportedCharacter character,Func<Vector3,Vector3> turn,string warning)\r\n {\r\n return new ImportedCharacter{Name=character.Name,SourcePath=character.SourcePath,SourceUnitCm=character.SourceUnitCm,SourceUpAxis=character.SourceUpAxis,\r\n HasExistingSkin=character.HasExistingSkin,ExistingBones=character.ExistingBones.Select(b=>b with{Position=turn(b.Position)}).ToArray(),\r\n Meshes=character.Meshes.Select(m=>m with{Vertices=m.Vertices.Select(turn).ToArray(),CornerNormals=m.CornerNormals.Select(turn).ToArray()}).ToArray(),\r\n Materials=character.Materials,EmbeddedTextures=character.EmbeddedTextures,\r\n ImportWarnings=character.ImportWarnings.Append(warning).ToArray()};\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/HumanoidUp.cs",
"FileName": "HumanoidUp.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Check declared up against a head, torso and paired lower-limb\r\n/// cross sections. Ambiguous or open geometry retains the imported axes.</summary>\r\ninternal static class HumanoidUp\r\n{\r\n internal static Vector3 Find(ImportedCharacter character)\r\n {\r\n var points=character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();\r\n if(points.Length<100)return Vector3.UnitY;\r\n var minimum=points.Aggregate(Vector3.Min);var maximum=points.Aggregate(Vector3.Max);\r\n var center=(minimum+maximum)*.5f;float reach=(maximum-minimum).Length();\r\n bool Supports(Vector3 up)\r\n {\r\n float bottom=points.Min(p=>Vector3.Dot(p,up)),height=points.Max(p=>Vector3.Dot(p,up))-bottom;\r\n if(height<.001f)return false;\r\n MeshSections.Section[] Sections(float fraction)\r\n {\r\n var origin=center+up*(bottom+height*fraction-Vector3.Dot(center,up));\r\n return MeshSections.Cut(character,origin,up,reach,height*1e-5f)\r\n .Where(s=>s.Area>height*height*1e-6f).OrderByDescending(s=>s.Area).ToArray();\r\n }\r\n var heads=Sections(.92f);\r\n if(heads.Length==0||heads[0].Area<heads.Sum(s=>s.Area)*.7f||heads[0].Radius>height*.3f)return false;\r\n var torsos=Sections(.60f);if(torsos.Length==0)return false;\r\n var head=heads[0].Center;var torso=torsos[0].Center;\r\n Vector3 Horizontal(Vector3 p)=>p-up*Vector3.Dot(p,up);\r\n if(Horizontal(head-torso).Length()>height*.22f)return false;\r\n Vector3? previous=null;int evidence=0;\r\n foreach(float fraction in new[]{.25f,.35f})\r\n {\r\n var limbs=Sections(fraction);float best=float.PositiveInfinity;Vector3 direction=default;\r\n for(int i=0;i<limbs.Length;i++)for(int j=i+1;j<limbs.Length;j++)\r\n {\r\n var a=limbs[i];var b=limbs[j];var delta=b.Center-a.Center;float length=delta.Length();\r\n if(length<height*.06f||length>height*.45f||Math.Min(a.Area,b.Area)<Math.Max(a.Area,b.Area)*.2f)continue;\r\n if(length<(a.Radius+b.Radius)*1.1f)continue;\r\n float offset=Horizontal((a.Center+b.Center)*.5f-torso).Length();\r\n if(offset>height*.2f||offset>=best)continue;\r\n direction=delta/length;best=offset;\r\n }\r\n if(!float.IsFinite(best))continue;\r\n if(previous is {} prior&&Math.Abs(Vector3.Dot(prior,direction))<.85f)return false;\r\n previous=direction;evidence++;\r\n }\r\n return evidence>0;\r\n }\r\n // A plausible declared up always wins. Correct only a unique supported\r\n // alternative; the longest model dimension alone may simply be its arms.\r\n if(Supports(Vector3.UnitY))return Vector3.UnitY;\r\n Vector3? candidate=null;\r\n foreach(var up in new[]{Vector3.UnitX,-Vector3.UnitX,-Vector3.UnitY,Vector3.UnitZ,-Vector3.UnitZ})\r\n {\r\n if(!Supports(up))continue;\r\n if(candidate is not null)return Vector3.UnitY;\r\n candidate=up;\r\n }\r\n return candidate??Vector3.UnitY;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/MeshSections.cs",
"FileName": "MeshSections.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\nusing Vector2=System.Numerics.Vector2;\r\n\r\n/// <summary>Closed triangle-plane contours, with area centers independent of vertex density.</summary>\r\ninternal static class MeshSections\r\n{\r\n internal record Section(Vector3 Center,float Area,float Radius,float MinimumRadius);\r\n internal static Section[] Cut(ImportedCharacter character,Vector3 origin,Vector3 normal,float reach,float tolerance)\r\n {\r\n normal=Vector3.Normalize(normal);var u=Vector3.Normalize(Vector3.Cross(normal,Math.Abs(normal.Z)>.9f?Vector3.UnitY:Vector3.UnitZ));var v=Vector3.Cross(normal,u);\r\n var points=new List<Vector2>();var edges=new HashSet<(int,int)>();var cells=new Dictionary<(int,int),List<int>>();\r\n int Node(Vector3 position)\r\n {\r\n var delta=position-origin;var p=new Vector2(Vector3.Dot(delta,u),Vector3.Dot(delta,v));\r\n int x=(int)Math.Floor(p.X/tolerance),y=(int)Math.Floor(p.Y/tolerance);\r\n for(int i=-1;i<=1;i++)for(int j=-1;j<=1;j++)if(cells.TryGetValue((x+i,y+j),out var nearby))\r\n foreach(int n in nearby)if(Vector2.DistanceSquared(points[n],p)<=tolerance*tolerance)return n;\r\n int index=points.Count;points.Add(p);if(!cells.TryGetValue((x,y),out var bucket))cells[(x,y)]=bucket=[];bucket.Add(index);return index;\r\n }\r\n var cut=new Vector3[3];\r\n foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))for(int t=0;t<mesh.Triangles.Length;t+=3)\r\n {\r\n int count=0;\r\n for(int e=0;e<3;e++)\r\n {\r\n var a=mesh.Vertices[mesh.Triangles[t+e]];var b=mesh.Vertices[mesh.Triangles[t+(e+1)%3]];\r\n float da=Vector3.Dot(a-origin,normal),db=Vector3.Dot(b-origin,normal);\r\n if((da<=0&&db>0)||(db<=0&&da>0))cut[count++]=Vector3.Lerp(a,b,da/(da-db));\r\n }\r\n if(count!=2||Vector3.Distance(cut[0],origin)>reach||Vector3.Distance(cut[1],origin)>reach)continue;\r\n int first=Node(cut[0]),second=Node(cut[1]);if(first!=second)edges.Add((Math.Min(first,second),Math.Max(first,second)));\r\n }\r\n var neighbors=points.Select(_=>new List<int>()).ToArray();foreach(var(a,b)in edges){neighbors[a].Add(b);neighbors[b].Add(a);}\r\n var sections=new List<Section>();var seen=new bool[points.Count];\r\n for(int start=0;start<points.Count;start++)\r\n {\r\n if(seen[start])continue;var component=new List<int>();var queue=new Queue<int>();queue.Enqueue(start);seen[start]=true;\r\n while(queue.TryDequeue(out int n)){component.Add(n);foreach(int next in neighbors[n])if(!seen[next]){seen[next]=true;queue.Enqueue(next);}}\r\n if(component.Count<6||component.Any(n=>neighbors[n].Count!=2))continue;\r\n var polygon=new List<Vector2>();int previous=-1,current=start;\r\n do{polygon.Add(points[current]);int next=neighbors[current].First(n=>n!=previous);previous=current;current=next;}while(current!=start&&polygon.Count<=component.Count);\r\n if(current!=start||polygon.Count!=component.Count)continue;\r\n float twiceArea=0;var weighted=Vector2.Zero;\r\n for(int i=0;i<polygon.Count;i++){var a=polygon[i];var b=polygon[(i+1)%polygon.Count];float cross=a.X*b.Y-b.X*a.Y;twiceArea+=cross;weighted+=(a+b)*cross;}\r\n if(Math.Abs(twiceArea)<tolerance*tolerance)continue;\r\n var center=weighted/(3*twiceArea);float radius=polygon.Max(p=>Vector2.Distance(center,p));\r\n float minimum=float.PositiveInfinity;\r\n for(int i=0;i<polygon.Count;i++)\r\n {\r\n var a=polygon[i];var b=polygon[(i+1)%polygon.Count];var ab=b-a;\r\n float t=Math.Clamp(Vector2.Dot(center-a,ab)/Math.Max(ab.LengthSquared(),1e-12f),0,1);minimum=Math.Min(minimum,Vector2.Distance(center,a+ab*t));\r\n }\r\n sections.Add(new(origin+u*center.X+v*center.Y,Math.Abs(twiceArea)*.5f,radius,minimum));\r\n }\r\n return sections.ToArray();\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Analysis/FingerAlignment.cs",
"FileName": "FingerAlignment.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "#nullable enable annotations\r\nnamespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Centers non-thumb chains on their own closed mesh sections, preserving\r\n/// supported joint spacing and repairing chains collapsed near a fingertip.</summary>\r\npublic static class FingerAlignment\r\n{\r\n public static int Refine(ImportedCharacter character,Anatomy anatomy,string side)\r\n {\r\n if(side is not (\"L\" or \"R\"))throw new ArgumentException(\"Unknown hand side.\");\r\n var fingers=new[]{\"Index\",\"Middle\",\"Ring\",\"Pinky\"};\r\n var chains=fingers.Select(f=>Enumerable.Range(1,3).Select(i=>f+i+\".\"+side).Append(f+\"Tip.\"+side).ToArray())\r\n .Where(roles=>roles.All(r=>anatomy.Points.TryGetValue(r,out var p)&&!p.Corrected&&p.Confidence>=.35f)).ToArray();\r\n if(chains.Length==0)return 0;\r\n var wrist=anatomy[\"Hand.\"+side];float h=anatomy.Height;\r\n float reach=chains.SelectMany(r=>r).Max(r=>Vector3.Distance(wrist,anatomy[r]))+h*.035f;\r\n var local=HandSurface(character,wrist,reach);\r\n // Containment uses the original complete shells, never the cropped analysis surface.\r\n var volume=new SurfaceVisibility(character.Meshes.Where(m=>m.Kind==MeshKind.Body),h*.00001f);\r\n int changed=0;\r\n foreach(var roles in chains)\r\n {\r\n var previous=roles.Select(r=>anatomy[r]).ToArray();var fitted=Fit(local,volume,wrist,previous,h);\r\n if(fitted is null)continue;\r\n for(int i=0;i<3;i++)anatomy.Points[roles[i]]=anatomy.Points[roles[i]] with{Position=fitted[i]};\r\n changed++;\r\n }\r\n return changed;\r\n }\r\n static Vector3[]? Fit(ImportedCharacter local,SurfaceVisibility volume,Vector3 wrist,Vector3[] previous,float h)\r\n {\r\n var tip=previous[3];var inward=wrist-tip;if(inward.LengthSquared()<h*h*.000001f)return null;\r\n var axis=Vector3.Normalize(inward);var center=tip;float step=h*.0015f;\r\n var path=new List<Vector3>{tip};var radii=new List<float>();var areas=new List<float>();bool ended=false;\r\n for(int i=0;i<75;i++)\r\n {\r\n var seed=center+axis*step;\r\n MeshSections.Section? section=null;\r\n // A plane through a mesh vertex can produce an ambiguous contour.\r\n // Nearby parallel cuts recover it without changing or welding geometry.\r\n foreach(float offset in new[]{0f,.15f,-.15f,.35f,-.35f})\r\n {\r\n section=MeshSections.Cut(local,seed+axis*(step*offset),axis,h*.035f,h*.00001f)\r\n .Where(s=>s.Radius>h*.0007f&&s.Radius<h*.016f&&Vector3.Distance(s.Center,seed)<h*.018f)\r\n .MinBy(s=>Vector3.DistanceSquared(s.Center,seed));\r\n if(section is not null)break;\r\n }\r\n if(section is null){ended=true;break;}\r\n var difference=section.Center-center;\r\n if(path.Count>1&&difference.Length()>h*.006f){ended=true;break;}\r\n if(path.Count>5&§ion.Area>areas.TakeLast(3).Average()*1.8f){ended=true;break;}\r\n if(path.Count>2&&volume.Blocked(center,section.Center,h*.00001f)){ended=true;break;}\r\n if(difference.LengthSquared()<1e-12f)return null;\r\n var tangent=Vector3.Normalize(difference);\r\n if(path.Count>1&&Vector3.Dot(axis,tangent)<.3f){ended=true;break;}\r\n center=section.Center;path.Add(center);radii.Add(section.Radius);areas.Add(section.Area);\r\n if(path.Count>2)axis=Vector3.Normalize(Vector3.Lerp(axis,tangent,.25f));\r\n if(Vector3.Distance(center,tip)>inward.Length()*.8f)return null;\r\n }\r\n if(path.Count<8||!ended)return null;\r\n var extension=center+axis*radii[^1]*.5f;\r\n if(volume.Contains(extension,h*.00001f)&&!volume.Blocked(center,extension,h*.00001f))path.Add(extension);\r\n path.Reverse();var distances=new float[path.Count];\r\n for(int i=1;i<path.Count;i++)distances[i]=distances[i-1]+Vector3.Distance(path[i-1],path[i]);\r\n float coverage=Vector3.Distance(path[0],tip)/Vector3.Distance(previous[0],tip);\r\n if(coverage<.6f)return null;\r\n // The webbing can end the trace before a well-supported palm knuckle.\r\n // Keep that base and still center the distal joints on the recovered digit.\r\n bool retainBase=coverage<.85f;\r\n (Vector3 Point,float Offset) Project(Vector3 point)\r\n {\r\n float best=float.PositiveInfinity,offset=0;var result=point;\r\n for(int i=1;i<path.Count;i++)\r\n {\r\n var closest=Geometry.ClosestOnSegment(point,path[i-1],path[i]);float distance=Vector3.DistanceSquared(point,closest);\r\n if(distance>=best)continue;best=distance;result=closest;offset=distances[i-1]+Vector3.Distance(closest,path[i-1]);\r\n }\r\n return(result,offset);\r\n }\r\n // A centered chain does not need a new proportion-based fit.\r\n if(previous.Skip(retainBase?1:0).Take(retainBase?2:3).Average(p=>Vector3.Distance(p,Project(p).Point))<h*.00025f)return null;\r\n var fitted=new[]{0f,.5f,.78f,1f}.Select(f=>\r\n {\r\n float target=distances[^1]*f;int i=Array.FindIndex(distances,d=>d>=target);if(i<=0)return path[0];\r\n return Vector3.Lerp(path[i-1],path[i],(target-distances[i-1])/Math.Max(distances[i]-distances[i-1],1e-8f));\r\n }).ToArray();\r\n if(retainBase)fitted[0]=previous[0];\r\n if(Vector3.Distance(previous[0],tip)>=Vector3.Distance(fitted[0],tip)*.6f)\r\n {\r\n var second=Project(previous[1]);var third=Project(previous[2]);\r\n if(second.Offset<h*.001f||third.Offset-second.Offset<h*.001f||distances[^1]-third.Offset<h*.001f)return null;\r\n fitted[1]=second.Point;fitted[2]=third.Point;\r\n }\r\n for(int bone=0;bone<3;bone++)for(int sample=0;sample<9;sample++)\r\n if(!volume.Contains(Vector3.Lerp(fitted[bone],fitted[bone+1],sample/9f),h*.00001f))return null;\r\n return fitted;\r\n }\r\n static ImportedCharacter HandSurface(ImportedCharacter character,Vector3 wrist,float radius)\r\n {\r\n var parts=new List<MeshPart>();float squared=radius*radius;\r\n foreach(var mesh in character.Meshes.Where(m=>m.Kind==MeshKind.Body))\r\n {\r\n var triangles=new List<int>();\r\n for(int i=0;i<mesh.Triangles.Length;i+=3)\r\n {\r\n var a=mesh.Vertices[mesh.Triangles[i]];var b=mesh.Vertices[mesh.Triangles[i+1]];var c=mesh.Vertices[mesh.Triangles[i+2]];\r\n var nearest=Vector3.Clamp(wrist,Vector3.Min(a,Vector3.Min(b,c)),Vector3.Max(a,Vector3.Max(b,c)));\r\n if(Vector3.DistanceSquared(nearest,wrist)>squared)continue;\r\n triangles.Add(mesh.Triangles[i]);triangles.Add(mesh.Triangles[i+1]);triangles.Add(mesh.Triangles[i+2]);\r\n }\r\n if(triangles.Count>0)parts.Add(new(mesh.Name,mesh.Vertices,triangles.ToArray(),MeshKind.Body));\r\n }\r\n return new ImportedCharacter{Meshes=parts.ToArray()};\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Rigging/SkinningNormals.cs",
"FileName": "SkinningNormals.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Angle-weighted outward normals for closed, consistently oriented\r\n/// components. Uncertain/open surfaces contribute no directional prior.</summary>\r\ninternal static class SkinningNormals\r\n{\r\n internal static Vector3[] ClosedSurface(Vector3[] points,int[] triangles)\r\n {\r\n var mesh=new MeshPart(\"Skinning surface\",points,triangles,MeshKind.Body);\r\n var components=Geometry.Components(mesh);int count=components.Max()+1;\r\n var origins=new Vector3[count];var assigned=new bool[count];\r\n for(int v=0;v<points.Length;v++)if(!assigned[components[v]]){origins[components[v]]=points[v];assigned[components[v]]=true;}\r\n var volumes=new double[count];var closed=Enumerable.Repeat(true,count).ToArray();\r\n var normals=new Vector3[points.Length];var edges=new Dictionary<(int,int),(int Count,int Direction)>();\r\n for(int t=0;t<triangles.Length;t+=3)\r\n {\r\n int a=triangles[t],b=triangles[t+1],c=triangles[t+2];if(a==b||a==c||b==c)continue;\r\n var normal=Vector3.Cross(points[b]-points[a],points[c]-points[a]);float area=normal.Length();\r\n if(area<1e-12f){closed[components[a]]=false;continue;}\r\n normal/=area;\r\n var origin=origins[components[a]];\r\n volumes[components[a]]+=Vector3.Dot(points[a]-origin,Vector3.Cross(points[b]-origin,points[c]-origin));\r\n for(int corner=0;corner<3;corner++)\r\n {\r\n int v=triangles[t+corner],n=triangles[t+(corner+1)%3],o=triangles[t+(corner+2)%3];\r\n var x=points[n]-points[v];var y=points[o]-points[v];\r\n normals[v]+=normal*MathF.Atan2(Vector3.Cross(x,y).Length(),Vector3.Dot(x,y));\r\n var key=(Math.Min(v,n),Math.Max(v,n));var edge=edges.GetValueOrDefault(key);\r\n edges[key]=(edge.Count+1,edge.Direction+(v<n?1:-1));\r\n }\r\n }\r\n foreach(var edge in edges)if(edge.Value.Count!=2||edge.Value.Direction!=0)closed[components[edge.Key.Item1]]=false;\r\n for(int v=0;v<normals.Length;v++)\r\n {\r\n int component=components[v];float length=normals[v].Length();\r\n normals[v]=closed[component]&&Math.Abs(volumes[component])>1e-12&&length>1e-6f\r\n ?normals[v]*(Math.Sign(volumes[component])/length):Vector3.Zero;\r\n }\r\n return normals;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Validation/RigValidator.cs",
"FileName": "RigValidator.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "#nullable enable annotations\r\nusing System.Numerics;\r\nnamespace HumanoidRigger;\r\nusing Vector3 = System.Numerics.Vector3;\r\n\r\npublic sealed record ValidationIssue(string Code,string Message,bool Error);\r\npublic sealed record StressResult(string Pose,float MaximumStretch,float MinimumAreaRatio,int NonFiniteVertices,float SourceEdgeLength=0,float DeformedEdgeLength=0,int NonFiniteMeasurements=0,int ReversedTriangles=0,float ReversedAreaFraction=0);\r\npublic sealed class ValidationReport\r\n{\r\n public List<ValidationIssue> Issues {get;}=[];\r\n public List<StressResult> StressTests {get;}=[];\r\n public List<StressResult> JointStressTests {get;}=[];\r\n public int Repairs {get;set;}\r\n public int RepairPasses {get;set;}\r\n public bool Passed=>Issues.All(i=>!i.Error) && StressTests.Count>0;\r\n}\r\npublic static class RigValidator\r\n{\r\n public static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig)\r\n =>ValidateAndRepair(character,rig,new ValidationGeometry(character));\r\n internal static ValidationReport ValidateAndRepair(ImportedCharacter character,GeneratedRig rig,ValidationGeometry geometry)\r\n {\r\n int repairs=0;\r\n foreach(var part in rig.Weights) for(int v=0;v<part.Length;v++)\r\n {\r\n var cleaned=Skinning.Cleanup(part[v],rig.Bones.Length,rig.Profile.MaximumInfluences);\r\n if(!cleaned.SequenceEqual(part[v])) {part[v]=cleaned;repairs++;}\r\n }\r\n var report=Validate(character,rig,null,geometry);report.Repairs=repairs;\r\n return SurfaceRepair.Improve(character,rig,WeightRepair.Improve(character,rig,report,geometry),geometry);\r\n }\r\n public static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig)\r\n =>Validate(character,rig,null);\r\n\r\n internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces)\r\n =>Validate(character,rig,faces,null);\r\n internal static ValidationReport Validate(ImportedCharacter character,GeneratedRig rig,BindTriangle[][]? faces,ValidationGeometry? geometry)\r\n {\r\n var height=geometry?.Height??character.AnatomicalHeight;var r=new ValidationReport();void Error(string code,string text)=>r.Issues.Add(new(code,text,true));\r\n try{rig.Profile.Validate();}catch(Exception e){Error(\"profile\",e.Message);return r;}\r\n var roles=new HashSet<string>();var names=new HashSet<string>();\r\n var body=geometry?.Body??character.Meshes.Where(m=>m.Kind==MeshKind.Body).SelectMany(m=>m.Vertices).ToArray();\r\n for(int i=0;i<rig.Bones.Length;i++)\r\n {\r\n var b=rig.Bones[i];\r\n if(!roles.Add(b.Role)||!names.Add(b.Name))Error(\"duplicate\",\"Duplicate bone assignment.\");\r\n bool validParent=b.Parent<i&&b.Parent>=-1;\r\n if(!validParent)Error(\"hierarchy\",\"Invalid skeleton hierarchy.\");\r\n if(!Geometry.Finite(b.Position)||!float.IsFinite(b.Rotation.LengthSquared())||Math.Abs(b.Rotation.LengthSquared()-1)>.001f)Error(\"frame\",\"Invalid bone orientation or position.\");\r\n if(b.Deform&&body.Length>0&&(geometry?.JointDistanceSquared(b.Position)??body.Min(p=>Vector3.DistanceSquared(p,b.Position)))>height*height*.15f*.15f)\r\n Error(\"joint-placement\",$\"{b.Role} is too far from the character. Check its landmark.\");\r\n var definition=rig.Profile.Bones.FirstOrDefault(d=>d.Role==b.Role);\r\n if(definition is null || definition.Name!=b.Name || !validParent || (b.Parent<0 ? null : rig.Bones[b.Parent].Role)!=definition.Parent)Error(\"mapping\",\"Skeleton differs from the selected profile.\");\r\n }\r\n foreach(var b in rig.Profile.Bones.Where(b=>b.Required))if(!roles.Contains(b.Role))Error(\"required\",$\"Missing {b.Role}.\");\r\n if(rig.Weights.Length!=character.Meshes.Length){Error(\"weights\",\"Missing mesh skinning.\");return r;}\r\n for(int p=0;p<rig.Weights.Length;p++)\r\n {\r\n if(rig.Weights[p].Length!=character.Meshes[p].Vertices.Length){Error(\"weights\",\"Skinning vertex count mismatch.\");continue;}\r\n foreach(var vertex in rig.Weights[p])\r\n {\r\n if(vertex.Length==0||vertex.Length>rig.Profile.MaximumInfluences)Error(\"influences\",\"Invalid influence count.\");\r\n if(vertex.Any(i=>i.Bone<0||i.Bone>=rig.Bones.Length||!float.IsFinite(i.Weight)||i.Weight<=0))Error(\"influences\",\"Invalid weight or bone index.\");\r\n if(Math.Abs(vertex.Sum(i=>i.Weight)-1)>.0001f)Error(\"normalization\",\"Weights do not sum to one.\");\r\n }\r\n }\r\n if(r.Issues.Any(i=>i.Error))return r;\r\n if(geometry?.Trunk?.HasBleeding(character,rig)==true){Error(\"weight-bleeding\",\"A weight repair reintroduced a remote torso attachment.\");return r;}\r\n var ends=RigGeometry.SegmentEnds(rig);\r\n var locality=new SkinningLocality(character,rig,ends);\r\n for(int p=0;p<character.Meshes.Length;p++)\r\n {\r\n var mesh=character.Meshes[p];if(mesh.Kind==MeshKind.Accessory)continue;\r\n bool remote=false;\r\n for(int v=0;v<mesh.Vertices.Length&&!remote;v++)\r\n foreach(var influence in rig.Weights[p][v])\r\n {\r\n if(!rig.Bones[influence.Bone].Deform){Error(\"nondeforming-influence\",\"Skinning references a non-deforming bone.\");remote=true;break;}\r\n if(influence.Weight>.05f&&Vector3.Distance(mesh.Vertices[v],Geometry.ClosestOnSegment(mesh.Vertices[v],rig.Bones[influence.Bone].Position,ends[influence.Bone]))>locality.Limit(influence.Bone,mesh.Vertices[v]))\r\n {Error(\"weight-region\",$\"Mesh '{mesh.Name}' is influenced by a distant anatomical region ({rig.Bones[influence.Bone].Role}).\");remote=true;break;}\r\n }\r\n }\r\n if(r.Issues.Any(i=>i.Error))return r;\r\n faces??=geometry?.Faces??character.Meshes.Select(BindTriangle.Measure).ToArray();\r\n var specifications=(geometry?.Poses??Deformation.Poses).ToArray();\r\n var tests=new StressResult[specifications.Length];\r\n Vector3[][] Buffers()=>character.Meshes.Select(m=>new Vector3[m.Vertices.Length]).ToArray();\r\n void Measure(int i,Vector3[][] buffer)\r\n {\r\n if(Deformation.IsApplicable(specifications[i],roles))tests[i]=MeasurePose(character,rig,specifications[i],faces,buffer,height);\r\n }\r\n // Poses read the same frozen weights and write separate buffers. Keep\r\n // report order and each pose's arithmetic serial and deterministic.\r\n int workers=RigWork.WorkerCount(character.Meshes.Sum(m=>m.Vertices.Length));\r\n RigWork.For(specifications.Length,workers,Buffers,Measure);\r\n for(int i=0;i<specifications.Length;i++)\r\n {\r\n var pose=specifications[i];var test=tests[i];\r\n if(test is null){r.Issues.Add(new(\"optional-pose\",$\"{pose.Name}: optional joints absent.\",false));continue;}\r\n r.StressTests.Add(test);\r\n if(test.ReversedTriangles>0)r.Issues.Add(new(\"surface-reversal\",$\"{pose.Name}: {test.ReversedTriangles} surface triangles reverse orientation.\",false));\r\n if(test.NonFiniteVertices>0||test.NonFiniteMeasurements>0)Error(\"deformation\",$\"{pose.Name}: deformation produced non-finite coordinates or measurements.\");\r\n else if(test.MaximumStretch>4||test.MinimumAreaRatio<.025f)Error(\"deformation\",$\"{pose.Name}: unsafe deformation (stretch {test.MaximumStretch:F2}, area ratio {test.MinimumAreaRatio:F3}).\");\r\n }\r\n return r;\r\n }\r\n internal static StressResult MeasurePose(ImportedCharacter character,GeneratedRig rig,StressPose pose,BindTriangle[][] faces,Vector3[][] deformed,float height)\r\n {\r\n var transforms=Deformation.BoneTransforms(rig,Deformation.JointRotations(rig,pose));\r\n var rotations=transforms.Rotations;\r\n Deformation.ApplyTransforms(character,rig,transforms.Positions,rotations,deformed);\r\n float stretch=1,minArea=1,sourceEdge=0,deformedEdge=0;int nonFinite=0,invalidMeasurements=0;\r\n int reversed=0;double surfaceArea=0,reversedArea=0;\r\n for(int p=0;p<character.Meshes.Length;p++)\r\n {\r\n var mesh=character.Meshes[p];var dst=deformed[p];nonFinite+=dst.Count(v=>!Geometry.Finite(v));\r\n foreach(var face in faces[p])\r\n {\r\n var i=face.A;var j=face.B;var k=face.C;\r\n var normal=face.Normal;\r\n var posedNormal=Vector3.Cross(dst[j]-dst[i],dst[k]-dst[i]);\r\n float area=face.Area,posedArea=posedNormal.Length();\r\n if(!float.IsFinite(area)||!float.IsFinite(posedArea))invalidMeasurements++;\r\n else if(area>height*height*1e-10f)\r\n {\r\n float ratio=posedArea/area;\r\n if(float.IsFinite(ratio))minArea=Math.Min(minArea,ratio);else invalidMeasurements++;\r\n surfaceArea+=area;\r\n float alignment=SurfaceOrientation.Alignment(normal,posedNormal,rig.Weights[p][i],rig.Weights[p][j],rig.Weights[p][k],rotations);\r\n if(alignment<SurfaceOrientation.ReversalLimit){reversed++;reversedArea+=area;}\r\n }\r\n for(int edge=0;edge<3;edge++)\r\n {\r\n var (a,b,length)=face.Edge(edge);var posedLength=Vector3.Distance(dst[a],dst[b]);\r\n if(!float.IsFinite(length)||!float.IsFinite(posedLength)){invalidMeasurements++;continue;}\r\n if(length<=height*1e-6f)continue;\r\n float ratio=posedLength/length;\r\n if(!float.IsFinite(ratio)){invalidMeasurements++;continue;}\r\n if(ratio>stretch){stretch=ratio;sourceEdge=length;deformedEdge=posedLength;}\r\n }\r\n }\r\n }\r\n return new(pose.Name,stretch,minArea,nonFinite,sourceEdge,deformedEdge,invalidMeasurements,reversed,surfaceArea>0?(float)(reversedArea/surfaceArea):0);\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/Core/Validation/TrunkSkinning.cs",
"FileName": "TrunkSkinning.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "namespace HumanoidRigger;\r\nusing Vector3=System.Numerics.Vector3;\r\n\r\n/// <summary>Refine axial skinning with normal-aware heat and compact joint\r\n/// support. Full deformation evidence must remain safe before accepting it.</summary>\r\ninternal static class TrunkSkinning\r\n{\r\n internal static ValidationReport Improve(ImportedCharacter character,GeneratedRig rig,ValidationReport initial)\r\n {\r\n var expected=Deformation.Poses.Where(p=>Deformation.IsApplicable(p,rig.Bones.Select(b=>b.Role).ToHashSet())).Select(p=>p.Name).Order().ToArray();\r\n if(!initial.Passed||!WeightRepair.HasCompleteEvidence(initial,expected))return initial;\r\n var region=new TrunkRegion(character,rig);if(!region.HasBleeding(character,rig))return initial;\r\n var original=rig.Weights;bool accepted=false;\r\n try\r\n {\r\n var heat=HeatSkinning.Candidates(character,rig,normalPrior:true,trunk:region).First();\r\n var geometry=new ValidationGeometry(character,trunk:region);\r\n foreach(float amount in new[]{1f,.5f,0f})\r\n {\r\n rig.Weights=Apply(character,rig,region,original,heat,amount);\r\n var candidate=RigValidator.ValidateAndRepair(character,rig,geometry);\r\n // Local surface repair may adjust a protected boundary. Never\r\n // accept a trial that silently reintroduces remote attachments.\r\n if(region.HasBleeding(character,rig)||!candidate.Passed||!WeightRepair.HasCompleteEvidence(candidate,expected)||\r\n candidate.StressTests.Zip(initial.StressTests).Any(p=>p.First.Pose!=p.Second.Pose||p.First.ReversedTriangles>p.Second.ReversedTriangles||p.First.ReversedAreaFraction>p.Second.ReversedAreaFraction+1e-7f))continue;\r\n candidate.Repairs+=initial.Repairs;candidate.RepairPasses+=initial.RepairPasses+1;\r\n accepted=true;return candidate;\r\n }\r\n }\r\n catch(InvalidOperationException e){initial.Issues.Add(new(\"skinning-candidate\",e.Message,false));}\r\n finally{if(!accepted)rig.Weights=original;}\r\n initial.Issues.Add(new(\"weight-bleeding\",\"Torso skinning still follows a remote joint. Check the shoulder, hip and neck landmarks.\",true));\r\n return initial;\r\n }\r\n static Influence[][][] Apply(ImportedCharacter character,GeneratedRig rig,TrunkRegion region,Influence[][][] source,Influence[][][] heat,float amount)\r\n {\r\n var result=source.Select(p=>(Influence[][])p.Clone()).ToArray();var ends=RigGeometry.SegmentEnds(rig);\r\n for(int p=0;p<result.Length;p++)for(int v=0;v<result[p].Length;v++)if(region.Vertices[p][v])\r\n {\r\n var point=character.Meshes[p].Vertices[v];float blend=amount*region.Blend(point);\r\n var values=new float[rig.Bones.Length];\r\n foreach(var w in source[p][v])values[w.Bone]+=w.Weight*(1-blend);\r\n foreach(var w in heat[p][v])values[w.Bone]+=w.Weight*blend;\r\n float removed=0;\r\n foreach(var attachment in region.Attachments)\r\n {\r\n float support=attachment.Support(point);\r\n for(int b=0;b<values.Length;b++)if(attachment.Moving[b]){removed+=values[b]*(1-support);values[b]*=support;}\r\n }\r\n float axial=heat[p][v].Where(w=>region.Axial[w.Bone]).Sum(w=>w.Weight);\r\n if(axial>1e-6f)foreach(var w in heat[p][v]){if(region.Axial[w.Bone])values[w.Bone]+=removed*w.Weight/axial;}\r\n else\r\n {\r\n int nearest=Enumerable.Range(0,rig.Bones.Length).Where(b=>region.Axial[b]).MinBy(b=>Vector3.DistanceSquared(point,Geometry.ClosestOnSegment(point,rig.Bones[b].Position,ends[b])));\r\n values[nearest]+=removed;\r\n }\r\n result[p][v]=Skinning.Cleanup(values,rig.Profile.MaximumInfluences);\r\n }\r\n return result;\r\n }\r\n}\r\n"
},
{
"Ident": "notpointless.chomnr_humanoid_rigger",
"Path": "Editor/UI/MaterialAssets.Formats.cs",
"FileName": "MaterialAssets.Formats.cs",
"PackageType": "library",
"CodeKind": "Editor",
"AssetVersionId": 381682,
"Code": "using SkiaSharp;\r\nnamespace HumanoidRigger.Editor;\r\n\r\ninternal static partial class MaterialAssets\r\n{\r\n // Work on copies in the output/cache directory. glTF packs roughness in G\r\n // and metalness in B; Source 2 and MTL consume separate grayscale maps.\r\n internal static SourceMaterial[] PrepareFormats(SourceMaterial[] source,string directory,ExportFormats formats)\r\n {\r\n bool portable=(formats&(ExportFormats.Gltf|ExportFormats.Glb))!=0;\r\n var converted=new Dictionary<string,string>(StringComparer.OrdinalIgnoreCase);\r\n string Png(string path)\r\n {\r\n if(path is null||Path.GetExtension(path).ToLowerInvariant() is \".png\" or \".jpg\" or \".jpeg\")return path;\r\n if(converted.TryGetValue(path,out var result))return result;\r\n using var bitmap=Decode(path);result=\"textures/portable_\"+converted.Count+\".png\";Save(bitmap,result);converted.Add(path,result);return result;\r\n }\r\n SKBitmap Decode(string path)=>SKBitmap.Decode(Path.Combine(directory,path))??throw new FormatException(\"Cannot decode texture '\"+path+\"'.\");\r\n void Save(SKBitmap bitmap,string path)\r\n {\r\n string output=Path.Combine(directory,path);Directory.CreateDirectory(Path.GetDirectoryName(output));\r\n using var image=SKImage.FromBitmap(bitmap);using var data=image.Encode(SKEncodedImageFormat.Png,100);using var stream=File.Create(output);data.SaveTo(stream);\r\n }\r\n static SKColor Sample(SKBitmap bitmap,int x,int y,int width,int height,SKColor fallback)=>bitmap is null?fallback:bitmap.GetPixel(Math.Min(bitmap.Width-1,x*bitmap.Width/width),Math.Min(bitmap.Height-1,y*bitmap.Height/height));\r\n static byte Channel(float value)=>(byte)Math.Clamp((int)MathF.Round(value),0,255);\r\n return source.Select((original,index)=>\r\n {\r\n var m=ConvertSpecularGlossiness(original with{},directory,index);\r\n if(m.AuthoredPbr||m.AuthoredEmission)\r\n {\r\n float peak=Math.Max(1,Math.Max(m.EmissiveFactor.X,Math.Max(m.EmissiveFactor.Y,m.EmissiveFactor.Z)));\r\n m.EmissiveFactor/=peak;m.EmissiveStrength*=peak;\r\n }\r\n string WriteMap(string suffix,int width,int height,Func<int,int,SKColor> pixel)\r\n {\r\n using var bitmap=new SKBitmap(width,height,SKColorType.Rgba8888,SKAlphaType.Unpremul);\r\n for(int y=0;y<height;y++)for(int x=0;x<width;x++)bitmap.SetPixel(x,y,pixel(x,y));\r\n string path=\"textures/pbr_\"+index+\"_\"+suffix+\".png\";Save(bitmap,path);return path;\r\n }\r\n if(m.AuthoredPbr)\r\n {\r\n using var packed=m.MetallicRoughnessTexture is null?null:Decode(m.MetallicRoughnessTexture);\r\n int w=packed?.Width??1,h=packed?.Height??1;\r\n m.RoughnessTexture=WriteMap(\"roughness\",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Green??255)*m.RoughnessFactor);return new(v,v,v);});\r\n m.MetalnessTexture=WriteMap(\"metalness\",w,h,(x,y)=>{byte v=Channel((packed?.GetPixel(x,y).Blue??255)*m.MetallicFactor);return new(v,v,v);});\r\n }\r\n else if(portable&&(m.RoughnessTexture is not null||m.MetalnessTexture is not null))\r\n {\r\n using var rough=m.RoughnessTexture is null?null:Decode(m.RoughnessTexture);using var metal=m.MetalnessTexture is null?null:Decode(m.MetalnessTexture);\r\n int w=Math.Max(rough?.Width??1,metal?.Width??1),h=Math.Max(rough?.Height??1,metal?.Height??1);\r\n m.MetallicRoughnessTexture=WriteMap(\"metallic_roughness\",w,h,(x,y)=>new(255,Sample(rough,x,y,w,h,SKColors.White).Red,Sample(metal,x,y,w,h,SKColors.Black).Red));\r\n }\r\n if(m.NormalTexture is not null&&m.NormalScale!=1)\r\n {\r\n using var normal=Decode(m.NormalTexture);float strength=m.NormalScale;\r\n m.NormalTexture=WriteMap(\"normal\",normal.Width,normal.Height,(x,y)=>\r\n {\r\n var c=normal.GetPixel(x,y);\r\n var n=new System.Numerics.Vector3((c.Red/255f*2-1)*strength,(c.Green/255f*2-1)*strength,c.Blue/255f*2-1);\r\n n=n.LengthSquared()>1e-12f?System.Numerics.Vector3.Normalize(n):System.Numerics.Vector3.UnitZ;\r\n return new(Channel((n.X*.5f+.5f)*255),Channel((n.Y*.5f+.5f)*255),Channel((n.Z*.5f+.5f)*255),c.Alpha);\r\n });\r\n m.NormalScale=1;\r\n }\r\n if(m.AuthoredPbr&&m.OcclusionTexture is not null)\r\n {\r\n // glTF AO uses only R, even when roughness and metalness share\r\n // the same image. Source 2 needs a separate grayscale input.\r\n using var occlusion=Decode(m.OcclusionTexture);float strength=m.OcclusionStrength;\r\n m.OcclusionTexture=WriteMap(\"occlusion\",occlusion.Width,occlusion.Height,(x,y)=>\r\n {byte v=Channel(255+strength*(occlusion.GetPixel(x,y).Red-255));return new(v,v,v);});\r\n m.OcclusionStrength=1;\r\n }\r\n if((m.AuthoredPbr||m.AuthoredEmission)&&m.EmissiveTexture is null&&m.EmissiveFactor.LengthSquared()>0&&m.EmissiveStrength>0)\r\n m.EmissiveTexture=WriteMap(\"emission\",1,1,(x,y)=>SKColors.White);\r\n if(portable)\r\n {\r\n if(m.OpacityTexture is not null)\r\n {\r\n using var color=m.ColorTexture is null?null:Decode(m.ColorTexture);using var opacity=Decode(m.OpacityTexture);\r\n int w=Math.Max(color?.Width??1,opacity.Width),h=Math.Max(color?.Height??1,opacity.Height);\r\n bool packedAlpha=string.Equals(m.OpacityTexture,m.ColorTexture,StringComparison.OrdinalIgnoreCase);\r\n m.ColorTexture=WriteMap(\"rgba\",w,h,(x,y)=>{var c=Sample(color,x,y,w,h,SKColors.White);var a=Sample(opacity,x,y,w,h,SKColors.White);return new(c.Red,c.Green,c.Blue,packedAlpha?a.Alpha:Channel(c.Alpha*a.Red/255f));});\r\n }\r\n m.ColorTexture=Png(m.ColorTexture);m.NormalTexture=Png(m.NormalTexture);m.MetallicRoughnessTexture=Png(m.MetallicRoughnessTexture);m.OcclusionTexture=Png(m.OcclusionTexture);m.EmissiveTexture=Png(m.EmissiveTexture);\r\n }\r\n return m;\r\n }).ToArray();\r\n }\r\n}\r\n"
}
]
}