AutomaticWardrobeFit is a static utility that adapts arbitrary human clothing models to a unified male/female body surface at runtime. It reads a binary reference, parses model vertex/index buffers, computes nearest-surface mappings and morph targets, clears fabric intersections, and builds a fitted Model (optionally neck-only) with morph deltas for the engine.
using System;
using System.IO;
using System.Text;
using System.Runtime.InteropServices;
namespace CharacterEditor;
/// <summary>Fits loaded human clothes against shared body surfaces, without garment authoring files.</summary>
public static class AutomaticWardrobeFit
{
const string ReferencePath = "models/characterkit/wardrobe-reference.bin";
public const string Morph = "ce_body_female";
// Cache by resource identity and authored cut, never by the current slider value.
static readonly Dictionary<(Model, bool, bool), Model> fittedModels = new();
static readonly Dictionary<(Model, bool, bool), string> fittingFailures = new();
static Surface originalMale, unifiedMale, unifiedFemale;
static Surface headSurface;
static Model replacementNeck;
public static int CachedModels => fittedModels.Count;
public static void ClearCache()
{
fittedModels.Clear();
fittingFailures.Clear();
replacementNeck = null;
headSurface = null;
}
/// <summary>Keep the lower neck when clothing replaces the head bodygroup.</summary>
public static Model ReplacementNeck()
{
if (replacementNeck is not null) return replacementNeck;
References();
return replacementNeck = Build(Model.Load(UnifiedHuman.ModelPath), false, neckOnly: true);
}
public static bool IsFitted(Model model) => model?.Name.StartsWith("automatic-human-fit:", StringComparison.Ordinal) == true;
public static Model Fit(Model source, bool female, bool fitFace = true)
{
if (source is null || source.IsError || IsFitted(source) || source.BoneCount == 0 || source.Bounds.Mins.z > 60)
return source;
if (fittedModels.TryGetValue((source, female, fitFace), out var ready))
return ready;
if (fittingFailures.TryGetValue((source, female, fitFace), out var problem))
throw new InvalidOperationException(problem);
try
{
References();
var fitted = Build(source, female, fitFace: fitFace);
fittedModels[(source, female, fitFace)] = fitted;
return fitted;
}
catch (Exception error)
{
var message = "Cannot automatically fit " + source.Name + ": " + error.Message;
fittingFailures[(source, female, fitFace)] = message;
throw new InvalidOperationException(message, error);
}
}
static void References()
{
if (unifiedMale is not null)
return;
using var stream = new MemoryStream(FileSystem.Mounted.ReadAllBytes(ReferencePath).ToArray());
using var r = new BinaryReader(stream);
if (r.ReadInt32() != 1)
throw new InvalidOperationException("Unsupported human fitting reference.");
originalMale = ReadSurface(r);
unifiedMale = ReadSurface(r);
unifiedFemale = new(unifiedMale.Female, unifiedMale.Female, unifiedMale.Triangles, unifiedMale.Names, unifiedMale.Weights);
}
static Surface HeadSurface()
{
if (headSurface is not null) return headSurface;
var source = Model.Load(UnifiedHuman.ModelPath);
var buffers = ReadBuffers(source);
var points = new List<Vector3>();
var triangles = new List<int>();
for (int mi = 0; mi < buffers.Count; mi++)
{
var info = source.MeshInfo.Meshes[mi];
if (!info.Name.StartsWith("head", StringComparison.OrdinalIgnoreCase)) continue;
var buffer = buffers[mi];
int callIndex = 0;
foreach (var call in info.DrawCalls)
{
int vb = buffer.Vertices.Length == 1 ? 0 : callIndex;
int ib = buffer.Indices.Length == 1 ? 0 : callIndex;
var vertices = Vertices(buffer.Vertices[vb], null, buffer.Bones.Length);
var indices = Indices(buffer.Indices[ib]);
int offset = points.Count;
points.AddRange(vertices.Select(v => v.Position));
triangles.AddRange(indices.Skip(call.StartIndex).Take(call.Indices).Select(i => i + call.BaseVertex + offset));
callIndex++;
}
}
var p = points.ToArray();
return headSurface = new(p, p, triangles.ToArray(), Array.Empty<string>(), Array.Empty<(int, float)[]>());
}
static Vector3 V(BinaryReader r) => new(r.ReadSingle(), r.ReadSingle(), r.ReadSingle());
static Surface ReadSurface(BinaryReader r)
{
var names = new string[r.ReadInt32()];
for (int i = 0; i < names.Length; i++)
names[i] = Encoding.UTF8.GetString(r.ReadBytes(r.ReadInt32()));
var count = r.ReadInt32();
var triangles = new int[r.ReadInt32() * 3];
var p = new Vector3[count];
var f = new Vector3[count];
var weights = new (int, float)[count][];
for (int i = 0; i < count; i++)
{
p[i] = V(r);
f[i] = V(r);
weights[i] = new (int, float)[r.ReadInt32()];
for (int j = 0; j < weights[i].Length; j++)
weights[i][j] = (r.ReadInt32(), r.ReadSingle());
}
for (int i = 0; i < triangles.Length; i++)
triangles[i] = r.ReadInt32();
return new(p, f, triangles, names, weights);
}
[StructLayout(LayoutKind.Sequential)]
struct SkinVertex
{
[VertexLayout.Position]
public Vector3 Position;
[VertexLayout.Normal]
public Vector3 Normal;
[VertexLayout.Tangent]
public Vector4 Tangent;
[VertexLayout.TexCoord(0)]
public Vector4 UV;
[VertexLayout.TexCoord(1)]
public Vector4 UV2;
[VertexLayout.Color]
public Color32 Color;
[VertexLayout.BlendIndices]
public Color32 Indices;
[VertexLayout.BlendWeight]
public Color32 Weights;
}
readonly record struct Attribute(string Name, int Index, int Format, int Offset);
sealed record Buffer(int Count, int Stride, Attribute[] Attributes, byte[] Data);
sealed record Buffers(Buffer[] Vertices, Buffer[] Indices, string[] Bones);
// SaveToVmdl emits unpacked MBUF data, even when the installed resource uses
// compressed buffers. Reading that public serialization retains authored weights.
static List<Buffers> ReadBuffers(Model source)
{
var data = source.SaveToVmdl();
using var r = new BinaryReader(new MemoryStream(data));
r.BaseStream.Position = 8;
var table = 8 + r.ReadInt32();
var blocks = r.ReadInt32();
var result = new List<Buffers>();
for (int i = 0; i < blocks; i++)
{
r.BaseStream.Position = table + i * 12;
var tag = Encoding.ASCII.GetString(r.ReadBytes(4));
var anchor = (int)r.BaseStream.Position;
var offset = r.ReadInt32();
var size = r.ReadInt32();
if (tag == "MDAT")
{
r.BaseStream.Position = anchor + offset;
var bones = SerializedMeshBones.Read(r.ReadBytes(size));
result[^1] = result[^1] with
{
Bones = bones
};
continue;
}
if (tag != "MBUF")
continue;
var start = anchor + offset;
r.BaseStream.Position = start;
var vo = r.ReadInt32();
var vc = r.ReadInt32();
var io = r.ReadInt32();
var ic = r.ReadInt32();
result.Add(new(Enumerable.Range(0, vc).Select(j => ReadBuffer(r, start + vo + j * 24)).ToArray(), Enumerable.Range(0, ic).Select(j => ReadBuffer(r, start + 8 + io + j * 24)).ToArray(), null));
}
if (result.Count != source.MeshCount)
throw new InvalidOperationException($"{source.Name}: missing serialized mesh buffers.");
return result;
}
static Buffer ReadBuffer(BinaryReader r, int position)
{
r.BaseStream.Position = position;
var count = r.ReadInt32();
var stride = r.ReadInt32() & 0x3ffffff;
var ap = (int)r.BaseStream.Position;
var ao = r.ReadInt32();
var ac = r.ReadInt32();
var dp = (int)r.BaseStream.Position;
var offset = r.ReadInt32();
var bytes = r.ReadInt32();
if (count < 0 || count > 2000000 || stride < 1 || stride > 1024 || bytes != count * stride)
throw new InvalidOperationException("Unexpected serialized human clothing buffer.");
var attributes = new Attribute[ac];
for (int i = 0; i < ac; i++)
{
r.BaseStream.Position = ap + ao + i * 56;
var name = Encoding.ASCII.GetString(r.ReadBytes(32)).TrimEnd('\0');
attributes[i] = new(name, r.ReadInt32(), r.ReadInt32(), r.ReadInt32());
}
r.BaseStream.Position = dp + offset;
return new(count, stride, attributes, r.ReadBytes(bytes));
}
static float F(byte[] b, int p) => BitConverter.ToSingle(b, p);
static ushort U16(byte[] b, int p) => BitConverter.ToUInt16(b, p);
static uint U32(byte[] b, int p) => BitConverter.ToUInt32(b, p);
static Color32 Bytes(byte[] b, int p) => new(b[p], b[p + 1], b[p + 2], b[p + 3]);
static Vector4 Values(byte[] b, int p, int format)
{
float HalfAt(int i) => (float)BitConverter.UInt16BitsToHalf(U16(b, p + i * 2));
return format switch
{
2 => new(F(b, p), F(b, p + 4), F(b, p + 8), F(b, p + 12)),
6 => new(F(b, p), F(b, p + 4), F(b, p + 8), 0),
16 => new(F(b, p), F(b, p + 4), 0, 0),
10 => new(HalfAt(0), HalfAt(1), HalfAt(2), HalfAt(3)),
34 => new(HalfAt(0), HalfAt(1), 0, 0),
28 => new(b[p] / 255f, b[p + 1] / 255f, b[p + 2] / 255f, b[p + 3] / 255f),
_ => throw new InvalidOperationException("Unsupported clothing vertex format " + format)};
}
static SkinVertex[] Vertices(Buffer b, Vertex[] native, int boneCount)
{
var vertices = new SkinVertex[b.Count];
for (int i = 0; i < b.Count; i++)
{
var v = new SkinVertex
{
Color = new(255, 255, 255, 255),
Weights = new(255, 0, 0, 0)
};
foreach (var a in b.Attributes)
{
var p = i * b.Stride + a.Offset;
switch (a.Name)
{
case "POSITION":
var xyz = Values(b.Data, p, a.Format);
v.Position = new(xyz.x, xyz.y, xyz.z);
break;
case "TEXCOORD":
if (a.Index == 0)
v.UV = Values(b.Data, p, a.Format);
else if (a.Index == 1)
v.UV2 = Values(b.Data, p, a.Format);
break;
case "BLENDINDICES":
if (a.Format == 30)
v.Indices = Bytes(b.Data, p);
else if (a.Format == 12)
{
var ids = Enumerable.Range(0, 4).Select(j => (int)U16(b.Data, p + j * 2)).ToArray();
if (ids.Any(j => j >= boneCount || j > 255))
throw new InvalidOperationException("Clothing bone index exceeds fitting layout.");
v.Indices = new((byte)ids[0], (byte)ids[1], (byte)ids[2], (byte)ids[3]);
}
else
throw new InvalidOperationException("Unsupported clothing bone-index format " + a.Format);
break;
case "BLENDWEIGHT":
if (a.Format != 28)
throw new InvalidOperationException("Unsupported clothing weight format " + a.Format);
v.Weights = Bytes(b.Data, p);
break;
case "COLOR":
if (a.Format == 28)
v.Color = Bytes(b.Data, p);
break;
case "NORMAL":
if (a.Format == 6)
{
var n = Values(b.Data, p, a.Format);
v.Normal = new(n.x, n.y, n.z);
}
break;
case "TANGENT":
v.Tangent = Values(b.Data, p, a.Format);
break;
}
}
if (v.Normal.LengthSquared < .01f && native is not null && native.Length == b.Count && (native[i].Position - v.Position).Length < .001f)
{
v.Normal = native[i].Normal;
v.Tangent = native[i].Tangent;
}
if (new[]
{
v.Indices.r,
v.Indices.g,
v.Indices.b,
v.Indices.a
}.Any(j => j >= boneCount))
throw new InvalidOperationException("Serialized garment bone map does not match its skeleton.");
vertices[i] = v;
}
return vertices;
}
static int[] Indices(Buffer b) => Enumerable.Range(0, b.Count).Select(i => b.Stride == 2 ? (int)U16(b.Data, i * 2) : checked((int)U32(b.Data, i * 4))).ToArray();
static Vector3[] Normals(Vector3[] p, int[] indices)
{
var n = new Vector3[p.Length];
for (int i = 0; i + 2 < indices.Length; i += 3)
{
var a = indices[i];
var b = indices[i + 1];
var c = indices[i + 2];
var cross = Vector3.Cross(p[b] - p[a], p[c] - p[a]);
n[a] += cross;
n[b] += cross;
n[c] += cross;
}
return n.Select(v => v.Normal).ToArray();
}
static Vector4[] Tangents(SkinVertex[] vertices, Vector3[] p, Vector3[] normals, int[] indices)
{
var t = new Vector3[p.Length];
var b = new Vector3[p.Length];
for (int i = 0; i + 2 < indices.Length; i += 3)
{
int a = indices[i], c = indices[i + 1], d = indices[i + 2];
var e = p[c] - p[a];
var f = p[d] - p[a];
var uv = vertices[c].UV - vertices[a].UV;
var uw = vertices[d].UV - vertices[a].UV;
float determinant = uv.x * uw.y - uv.y * uw.x;
if (MathF.Abs(determinant) < 1e-8f)
continue;
var tangent = (e * uw.y - f * uv.y) / determinant;
var bitangent = (f * uv.x - e * uw.x) / determinant;
foreach (int id in new[]
{
a,
c,
d
}
)
{
t[id] += tangent;
b[id] += bitangent;
}
}
return Enumerable.Range(0, p.Length).Select(i =>
{
var n = normals[i];
var tangent = (t[i] - n * Vector3.Dot(n, t[i])).Normal;
return new Vector4(tangent, Vector3.Dot(Vector3.Cross(n, tangent), b[i]) < 0 ? -1 : 1);
}).ToArray();
}
static Model Build(Model source, bool female, bool neckOnly = false, bool fitFace = true)
{
var buffers = ReadBuffers(source);
var builder = Model.Builder.WithName("automatic-human-fit:" + source.Name + ":" + (neckOnly ? "replacement-neck" : female ? "female" : "male"));
for (int i = 0; i < source.BoneCount; i++)
{
var transform = source.GetBoneTransform(i);
var parent = source.GetBoneParent(i);
builder.AddBone(source.GetBoneName(i), transform.Position, transform.Rotation, parent < 0 ? null : source.GetBoneName(parent));
}
var native = source.GetVertices();
var baseMaterials = (source.MaterialGroupCount > 0 ? source.GetMaterials(0) : source.Materials).ToArray();
var meshMaterials = new List<Material>();
var work = new List<Action>();
var clothMale = new List<Vector3>();
var clothFemale = new List<Vector3>();
var clothIndices = new List<int>();
var head = neckOnly || !fitFace ? null : HeadSurface();
for (int mi = 0; mi < buffers.Count; mi++)
{
var info = source.MeshInfo.Meshes[mi];
if (neckOnly && !info.Name.StartsWith("head", StringComparison.OrdinalIgnoreCase)) continue;
var buffer = buffers[mi];
var boneMap = buffer.Bones.Select(name => Enumerable.Range(0, source.BoneCount).First(i => source.GetBoneName(i) == name)).ToArray();
int callIndex = 0;
foreach (var call in info.DrawCalls)
{
// SaveToVmdl emits individual buffers in draw-call order when a mesh uses
// separate streams (its metadata currently labels each stream as zero).
int vb = buffer.Vertices.Length == 1 ? 0 : callIndex;
int ib = buffer.Indices.Length == 1 ? 0 : callIndex;
if (vb >= buffer.Vertices.Length || ib >= buffer.Indices.Length)
throw new InvalidOperationException("Unexpected garment draw-call buffer mapping.");
var verts = Vertices(buffer.Vertices[vb], mi == 0 && buffer.Vertices.Length == 1 ? native : null, buffer.Bones.Length);
var indices = Indices(buffer.Indices[ib]);
for (int i = 0; i < verts.Length; i++)
{
var ids = verts[i].Indices;
verts[i].Indices = new((byte)boneMap[ids.r], (byte)boneMap[ids.g], (byte)boneMap[ids.b], (byte)boneMap[ids.a]);
}
callIndex++;
var selectedIndices = indices.Skip(call.StartIndex).Take(call.Indices).Select(i => i + call.BaseVertex).ToArray();
if (neckOnly)
selectedIndices = Enumerable.Range(0, selectedIndices.Length / 3)
.Where(t => Enumerable.Range(0, 3).All(j => verts[selectedIndices[t * 3 + j]].Position.z <= 60))
.SelectMany(t => selectedIndices.Skip(t * 3).Take(3)).ToArray();
if (selectedIndices.Length == 0) continue;
var used = selectedIndices.Distinct().ToArray();
var lookup = used.Select((id, i) => (id, i)).ToDictionary(x => x.id, x => x.i);
var localIndices = selectedIndices.Select(i => lookup[i]).ToArray();
var v = used.Select(i => verts[i]).ToArray();
var original = v.Select(x => x.Position).ToArray();
var male = new Vector3[v.Length];
var target = new Vector3[v.Length];
bool skin = neckOnly || call.Material?.Name.Contains("/bodies/", StringComparison.OrdinalIgnoreCase) == true;
for (int i = 0; i < v.Length; i++)
{
var p = original[i];
if (!skin && !neckOnly && fitFace && p.z >= 57 && p.z <= 73)
{
var hit = head.Nearest(p);
if (hit.Distance < 1.2f && hit.Distance < originalMale.Nearest(p).Distance)
{
var normal = head.FaceNormal(hit.Triangle);
float clearance = Vector3.Dot(p - hit.Point, normal);
// Close face coverings need space for the difference in falloff
// between the skin and fabric under inherited facial deformers.
float margin = .60f * Math.Clamp((62.2f - p.z) / 1.0f, 0, 1);
if (margin > 0) p += normal * Math.Clamp(margin - clearance, 0, .85f);
male[i] = target[i] = p;
continue;
}
}
if (p.z > 60)
{
male[i] = p;
target[i] = p;
continue;
}
var from = (neckOnly ? unifiedMale : female ? unifiedFemale : originalMale).Nearest(p);
var mapped = female || neckOnly ? from : unifiedMale.Nearest(from.Point);
var m = unifiedMale.Interpolate(mapped.Triangle, mapped.Barycentric);
var f = unifiedFemale.Interpolate(mapped.Triangle, mapped.Barycentric);
// Preserve authored clearance, including compressed skin under clothing.
var offset = p - from.Point;
bool snap = skin && offset.Length < .12f;
male[i] = m + (snap ? Vector3.Zero : offset);
target[i] = f + (snap ? Vector3.Zero : offset);
}
int clothOffset = clothMale.Count;
if (!skin)
{
clothIndices.AddRange(localIndices.Select(i => i + clothOffset));
clothMale.AddRange(male);
clothFemale.AddRange(target);
}
work.Add(() =>
{
if (!skin)
{
for (int i = 0; i < v.Length; i++)
{
male[i] = clothMale[clothOffset + i];
target[i] = clothFemale[clothOffset + i];
}
}
var originalNormals = Normals(original, localIndices);
var maleNormals = Normals(male, localIndices);
var femaleNormals = Normals(target, localIndices);
var tangents = Tangents(v, male, maleNormals, localIndices);
var morph = new MorphDelta[v.Length];
for (int i = 0; i < v.Length; i++)
{
if (v[i].Normal.LengthSquared < .01f)
v[i].Normal = originalNormals[i];
v[i].Normal = (v[i].Normal + maleNormals[i] - originalNormals[i]).Normal;
v[i].Tangent = tangents[i];
v[i].Position = male[i];
morph[i] = new(i, target[i] - male[i], femaleNormals[i] - maleNormals[i]);
}
var mesh = new Mesh(call.Material, (MeshPrimitiveType)call.PrimitiveType);
mesh.UvDensity = call.UvDensity;
mesh.CreateVertexBuffer(v.Length, v.ToList());
mesh.CreateIndexBuffer(localIndices.Length, localIndices.ToList());
mesh.AddMorph(Morph, morph);
mesh.Bounds = new BBox(male.Concat(target).Aggregate((a, b) => Vector3.Min(a, b)), male.Concat(target).Aggregate((a, b) => Vector3.Max(a, b)));
var lods = info.LodMask;
if (source.MeshInfo.LodCount == 0)
builder.AddMesh(mesh);
else
for (int lod = 0; lod < source.MeshInfo.LodCount; lod++)
if ((lods & (1 << lod)) != 0)
builder.AddMesh(mesh, lod);
meshMaterials.Add(call.Material);
});
}
}
if (clothMale.Count > 0)
{
var m = clothMale.ToArray();
var f = clothFemale.ToArray();
var t = clothIndices.ToArray();
ClearTriangles(unifiedMale, m, t);
ClearTriangles(unifiedFemale, f, t);
clothMale = m.ToList();
clothFemale = f.ToList();
}
foreach (var build in work)
build();
for (int group = 0; group < source.MaterialGroupCount; group++)
{
var materials = source.GetMaterials(group).ToArray();
var g = builder.AddMaterialGroup(source.GetMaterialGroupName(group));
foreach (var material in meshMaterials)
{
var index = Array.IndexOf(baseMaterials, material);
g.AddMaterial(index >= 0 && index < materials.Length ? materials[index] : material);
}
}
for (int lod = 1; lod < source.MeshInfo.LodSwitchDistances.Length; lod++)
builder.WithLodDistance(lod, source.MeshInfo.LodSwitchDistances[lod]);
return builder.Create();
}
static void ClearTriangles(Surface body, Vector3[] points, int[] triangles)
{
// A smooth spatial clearance field moves folded/layered fabric together.
// Local per-triangle pushes can pull an inner layer through its outer flap.
var source = points.ToArray();
var min = points.Aggregate((a, b) => Vector3.Min(a, b)) - Vector3.One * 1.2f;
var max = points.Aggregate((a, b) => Vector3.Max(a, b)) + Vector3.One * 1.2f;
var candidates = body.Points.Where(p => p.x >= min.x && p.x <= max.x && p.y >= min.y && p.y <= max.y && p.z >= min.z && p.z <= max.z).ToArray();
var pushes = new List<(Vector3 Point, Vector3 Push)>();
var fabric = new Surface(points, points, triangles, Array.Empty<string>(), Array.Empty<(int, float)[]>());
foreach (var p in candidates)
{
var hit = fabric.Nearest(p);
if (hit.Distance > 1.2f)
continue;
var w = hit.Barycentric;
if (MathF.Min(w.x, MathF.Min(w.y, w.z)) < .015f)
continue;
var n = body.FaceNormal(body.Nearest(p).Triangle);
if (MathF.Abs(Vector3.Dot(n, fabric.FaceNormal(hit.Triangle))) < .25f)
continue;
var penetration = Vector3.Dot(p - hit.Point, n) + .14f;
if (penetration <= 0 || penetration > 1.2f)
continue;
pushes.Add((hit.Point, n * MathF.Min(.7f, penetration + .04f)));
}
// Spatial buckets bound the search and apply identical displacement to UV
// duplicates, overlapping material sections, and neighbouring LOD vertices.
const float radius = 2.5f;
(int, int, int) Cell(Vector3 p) => ((int)MathF.Floor(p.x / radius), (int)MathF.Floor(p.y / radius), (int)MathF.Floor(p.z / radius));
var buckets = pushes.GroupBy(p => Cell(p.Point)).ToDictionary(g => g.Key, g => g.ToArray());
for (int i = 0; i < points.Length; i++)
{
var cell = Cell(source[i]);
Vector3 best = default;
for (int x = -1; x <= 1; x++)
for (int y = -1; y <= 1; y++)
for (int z = -1; z <= 1; z++)
{
if (!buckets.TryGetValue((cell.Item1 + x, cell.Item2 + y, cell.Item3 + z), out var values))
continue;
foreach (var p in values)
{
var d = (source[i] - p.Point).Length / radius;
if (d >= 1)
continue;
var falloff = 1 - d * d * (3 - 2 * d);
var push = p.Push * falloff;
if (push.LengthSquared > best.LengthSquared)
best = push;
}
}
points[i] += best;
}
}
readonly record struct Hit(int Triangle, Vector3 Barycentric, Vector3 Point, float Distance);
sealed class Surface
{
public readonly Vector3[] Points, Female;
public readonly int[] Triangles;
public readonly string[] Names;
public readonly (int, float)[][] Weights;
readonly Node tree;
sealed class Node
{
public Vector3 Min, Max;
public Node Left, Right;
public int[] Faces;
}
public Surface(Vector3[] p, Vector3[] f, int[] t, string[] names, (int, float)[][] weights)
{
Points = p;
Female = f;
Triangles = t;
Names = names;
Weights = weights;
tree = BuildTree(Enumerable.Range(0, t.Length / 3).ToArray());
}
Vector3 A(int t, int corner) => Points[Triangles[t * 3 + corner]];
Node BuildTree(int[] faces)
{
var p = faces.SelectMany(i => new[] { A(i, 0), A(i, 1), A(i, 2) }).ToArray();
var node = new Node
{
Min = p.Aggregate((a, b) => Vector3.Min(a, b)),
Max = p.Aggregate((a, b) => Vector3.Max(a, b))
};
if (faces.Length <= 12)
{
node.Faces = faces;
return node;
}
var extent = node.Max - node.Min;
int axis = extent.x > extent.y ? (extent.x > extent.z ? 0 : 2) : (extent.y > extent.z ? 1 : 2);
float Coord(Vector3 v) => axis == 0 ? v.x : axis == 1 ? v.y : v.z;
var sorted = faces.OrderBy(i => Coord(A(i, 0) + A(i, 1) + A(i, 2))).ToArray();
int half = sorted.Length / 2;
node.Left = BuildTree(sorted.Take(half).ToArray());
node.Right = BuildTree(sorted.Skip(half).ToArray());
return node;
}
public Vector3 Interpolate(int t, Vector3 w) => A(t, 0) * w.x + A(t, 1) * w.y + A(t, 2) * w.z;
public Vector3 FaceNormal(int t) => Vector3.Cross(A(t, 1) - A(t, 0), A(t, 2) - A(t, 0)).Normal;
static float Distance(Node n, Vector3 p) => (p - Vector3.Max(n.Min, Vector3.Min(n.Max, p))).LengthSquared;
public Hit Nearest(Vector3 p)
{
float best = float.MaxValue;
int face = 0;
Vector3 bary = default, point = default;
void Search(Node node)
{
if (Distance(node, p) > best)
return;
if (node.Faces is null)
{
var a = node.Left;
var b = node.Right;
if (Distance(a, p) > Distance(b, p))
(a, b) = (b, a);
Search(a);
Search(b);
return;
}
foreach (var t in node.Faces)
{
var w = Closest(p, A(t, 0), A(t, 1), A(t, 2));
var q = Interpolate(t, w);
var distance = (p - q).LengthSquared;
if (distance < best)
{
best = distance;
face = t;
bary = w;
point = q;
}
}
}
Search(tree);
return new(face, bary, point, MathF.Sqrt(best));
}
static Vector3 Closest(Vector3 p, Vector3 a, Vector3 b, Vector3 c)
{
var ab = b - a;
var ac = c - a;
var ap = p - a;
float d1 = Vector3.Dot(ab, ap), d2 = Vector3.Dot(ac, ap);
if (d1 <= 0 && d2 <= 0)
return new(1, 0, 0);
var bp = p - b;
float d3 = Vector3.Dot(ab, bp), d4 = Vector3.Dot(ac, bp);
if (d3 >= 0 && d4 <= d3)
return new(0, 1, 0);
float vc = d1 * d4 - d3 * d2;
if (vc <= 0 && d1 >= 0 && d3 <= 0)
{
var v = d1 / (d1 - d3);
return new(1 - v, v, 0);
}
var cp = p - c;
float d5 = Vector3.Dot(ab, cp), d6 = Vector3.Dot(ac, cp);
if (d6 >= 0 && d5 <= d6)
return new(0, 0, 1);
float vb = d5 * d2 - d1 * d6;
if (vb <= 0 && d2 >= 0 && d6 <= 0)
{
var w = d2 / (d2 - d6);
return new(1 - w, 0, w);
}
float va = d3 * d6 - d5 * d4;
if (va <= 0 && d4 - d3 >= 0 && d5 - d6 >= 0)
{
var w = (d4 - d3) / (d4 - d3 + d5 - d6);
return new(0, 1 - w, w);
}
float sum = va + vb + vc;
if (MathF.Abs(sum) < 1e-12f)
return new(1, 0, 0);
return new(va / sum, vb / sum, vc / sum);
}
}
}// Runtime fitting integration.