Files
LightlessClient/LightlessSync/Services/ModelDecimation/MdlDecimator.cs
2026-01-19 14:14:14 +09:00

3615 lines
133 KiB
C#

using LightlessSync.LightlessConfiguration.Configurations;
using Lumina.Data.Parsing;
using Lumina.Extensions;
using Microsoft.Extensions.Logging;
using Nano = Nanomesh;
using Penumbra.GameData.Files.ModelStructs;
using System.Buffers.Binary;
using BoneWeight = Nanomesh.BoneWeight;
using MdlFile = Penumbra.GameData.Files.MdlFile;
using MsLogger = Microsoft.Extensions.Logging.ILogger;
using Vector2 = Nanomesh.Vector2F;
using Vector3 = Nanomesh.Vector3F;
using Vector3d = Nanomesh.Vector3;
using Vector4 = Nanomesh.Vector4F;
namespace LightlessSync.Services.ModelDecimation;
// if you're coming from another sync service, then kindly fuck off. lightless ftw lil bro
internal static class MdlDecimator
{
private const int MaxStreams = 3;
private const int MaxUvChannels = 4;
private const int ReadRetryCount = 8;
private const int ReadRetryDelayMs = 250;
private static readonly HashSet<MdlFile.VertexUsage> SupportedUsages =
[
MdlFile.VertexUsage.Position,
MdlFile.VertexUsage.Normal,
MdlFile.VertexUsage.Tangent1,
MdlFile.VertexUsage.Tangent2,
MdlFile.VertexUsage.UV,
MdlFile.VertexUsage.Color,
MdlFile.VertexUsage.BlendWeights,
MdlFile.VertexUsage.BlendIndices,
];
private static readonly HashSet<MdlFile.VertexType> SupportedTypes =
[
MdlFile.VertexType.Single1,
MdlFile.VertexType.Single2,
MdlFile.VertexType.Single3,
MdlFile.VertexType.Single4,
MdlFile.VertexType.Half2,
MdlFile.VertexType.Half4,
MdlFile.VertexType.UByte4,
MdlFile.VertexType.NByte4,
MdlFile.VertexType.Short2,
MdlFile.VertexType.Short4,
MdlFile.VertexType.NShort2,
MdlFile.VertexType.NShort4,
MdlFile.VertexType.UShort2,
MdlFile.VertexType.UShort4,
];
public static bool TryDecimate(string sourcePath, string destinationPath, ModelDecimationSettings settings, MsLogger logger)
{
try
{
var tuning = settings.Advanced;
if (!TryReadModelBytes(sourcePath, logger, out var data))
{
logger.LogDebug("Skipping model decimation; source file locked or unreadable: {Path}", sourcePath);
return false;
}
var mdl = new MdlFile(data);
if (!mdl.Valid)
{
logger.LogDebug("Skipping model decimation; invalid mdl: {Path}", sourcePath);
return false;
}
if (mdl.LodCount != 1)
{
logger.LogDebug("Skipping model decimation; unsupported LOD count for {Path}", sourcePath);
return false;
}
if (HasShapeData(mdl))
{
logger.LogDebug("Skipping model decimation; shape/morph data present for {Path}", sourcePath);
return false;
}
const int lodIndex = 0;
var lod = mdl.Lods[lodIndex];
var meshes = mdl.Meshes.ToArray();
if (meshes.Length == 0)
{
logger.LogDebug("Skipping model decimation; no meshes for {Path}", sourcePath);
return false;
}
if (lod.MeshCount == 0)
{
logger.LogDebug("Skipping model decimation; no meshes for {Path}", sourcePath);
return false;
}
var lodMeshStart = (int)lod.MeshIndex;
var lodMeshEnd = lodMeshStart + lod.MeshCount;
if (lodMeshStart < 0 || lodMeshEnd > meshes.Length)
{
logger.LogDebug("Skipping model decimation; invalid LOD mesh range for {Path}", sourcePath);
return false;
}
Dictionary<int, PreprocessedMeshOutput> bodyMeshOverrides = [];
BodyCollisionData? bodyCollision = null;
if (settings.AvoidBodyIntersection)
{
if (!TryBuildBodyCollisionData(
mdl,
lodIndex,
lodMeshStart,
lodMeshEnd,
settings,
tuning,
out bodyCollision,
out bodyMeshOverrides,
logger))
{
bodyCollision = null;
}
}
var anyDecimated = false;
var newSubMeshes = new List<MdlStructs.SubmeshStruct>(mdl.SubMeshes.Length);
var newVertexBuffer = new List<byte>(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0);
var newIndexBuffer = new List<ushort>(mdl.IndexBufferSize[lodIndex] > 0 ? (int)(mdl.IndexBufferSize[lodIndex] / sizeof(ushort)) : 0);
var subMeshCursor = 0;
for (var meshIndex = 0; meshIndex < meshes.Length; meshIndex++)
{
var mesh = meshes[meshIndex];
var meshSubMeshes = mdl.SubMeshes
.Skip(mesh.SubMeshIndex)
.Take(mesh.SubMeshCount)
.ToArray();
var meshIndexBase = newIndexBuffer.Count;
var vertexBufferBase = newVertexBuffer.Count;
MeshStruct updatedMesh;
MdlStructs.SubmeshStruct[] updatedSubMeshes;
byte[][] vertexStreams;
int[] indices;
bool decimated;
if (bodyMeshOverrides.TryGetValue(meshIndex, out var bodyOverride))
{
updatedMesh = bodyOverride.Mesh;
updatedSubMeshes = bodyOverride.SubMeshes;
vertexStreams = bodyOverride.VertexStreams;
indices = bodyOverride.Indices;
decimated = bodyOverride.Decimated;
updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase);
}
else if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd
&& TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, settings, tuning, bodyCollision,
out updatedMesh,
out updatedSubMeshes,
out vertexStreams,
out indices,
out decimated,
logger))
{
updatedSubMeshes = OffsetSubMeshes(updatedSubMeshes, meshIndexBase);
}
else
{
if (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd)
{
logger.LogDebug("Skipping decimation for mesh {MeshIndex} in {Path}", meshIndex, sourcePath);
}
updatedMesh = mesh;
updatedSubMeshes = CopySubMeshes(meshSubMeshes, meshIndexBase, mesh.StartIndex);
vertexStreams = CopyVertexStreams(mdl, lodIndex, mesh);
indices = ReadIndices(mdl, lodIndex, mesh);
decimated = false;
}
anyDecimated |= decimated;
var vertexCount = updatedMesh.VertexCount;
var streamSizes = new int[MaxStreams];
for (var stream = 0; stream < MaxStreams; stream++)
{
var stride = updatedMesh.VertexBufferStride(stream);
if (stride > 0 && vertexCount > 0)
{
streamSizes[stream] = stride * vertexCount;
}
}
updatedMesh.VertexBufferOffset1 = (uint)vertexBufferBase;
updatedMesh.VertexBufferOffset2 = (uint)(vertexBufferBase + streamSizes[0]);
updatedMesh.VertexBufferOffset3 = (uint)(vertexBufferBase + streamSizes[0] + streamSizes[1]);
newVertexBuffer.AddRange(vertexStreams[0]);
newVertexBuffer.AddRange(vertexStreams[1]);
newVertexBuffer.AddRange(vertexStreams[2]);
updatedMesh.StartIndex = (uint)meshIndexBase;
updatedMesh.SubMeshIndex = (ushort)subMeshCursor;
updatedMesh.SubMeshCount = (ushort)updatedSubMeshes.Length;
updatedMesh.IndexCount = (uint)indices.Length;
meshes[meshIndex] = updatedMesh;
newSubMeshes.AddRange(updatedSubMeshes);
subMeshCursor += updatedSubMeshes.Length;
newIndexBuffer.AddRange(indices.Select(static i => (ushort)i));
}
if (!anyDecimated)
{
logger.LogDebug("Skipping model decimation; no eligible meshes for {Path}", sourcePath);
return false;
}
var indexBytes = BuildIndexBytes(newIndexBuffer);
mdl.Meshes = meshes;
mdl.SubMeshes = [.. newSubMeshes];
mdl.VertexOffset[lodIndex] = 0;
mdl.IndexOffset[lodIndex] = (uint)newVertexBuffer.Count;
mdl.VertexBufferSize[lodIndex] = (uint)newVertexBuffer.Count;
mdl.IndexBufferSize[lodIndex] = (uint)indexBytes.Length;
mdl.Lods[lodIndex] = mdl.Lods[lodIndex] with
{
VertexDataOffset = 0,
VertexBufferSize = (uint)newVertexBuffer.Count,
IndexDataOffset = (uint)newVertexBuffer.Count,
IndexBufferSize = (uint)indexBytes.Length,
};
for (var clearIndex = 1; clearIndex < mdl.VertexOffset.Length; clearIndex++)
{
mdl.VertexOffset[clearIndex] = 0;
mdl.IndexOffset[clearIndex] = 0;
mdl.VertexBufferSize[clearIndex] = 0;
mdl.IndexBufferSize[clearIndex] = 0;
if (clearIndex < mdl.Lods.Length)
{
mdl.Lods[clearIndex] = mdl.Lods[clearIndex] with
{
VertexDataOffset = 0,
VertexBufferSize = 0,
IndexDataOffset = 0,
IndexBufferSize = 0,
};
}
}
mdl.RemainingData = [.. newVertexBuffer, .. indexBytes];
var outputData = mdl.Write();
Directory.CreateDirectory(Path.GetDirectoryName(destinationPath)!);
var tempPath = destinationPath + ".tmp";
File.WriteAllBytes(tempPath, outputData);
File.Move(tempPath, destinationPath, overwrite: true);
return true;
}
catch (Exception ex)
{
logger.LogWarning(ex, "Failed to decimate model {Path}", sourcePath);
return false;
}
}
private static bool TryReadModelBytes(string sourcePath, MsLogger logger, out byte[] data)
{
Exception? lastError = null;
for (var attempt = 0; attempt < ReadRetryCount; attempt++)
{
try
{
data = ReadAllBytesShared(sourcePath);
return true;
}
catch (IOException ex)
{
lastError = ex;
}
catch (UnauthorizedAccessException ex)
{
lastError = ex;
}
if (attempt < ReadRetryCount - 1)
{
Thread.Sleep(ReadRetryDelayMs);
}
}
if (lastError != null)
{
logger.LogDebug(lastError, "Failed to read model for decimation after {Attempts} attempts: {Path}", ReadRetryCount, sourcePath);
}
data = [];
return false;
}
private static byte[] ReadAllBytesShared(string sourcePath)
{
using var stream = new FileStream(sourcePath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
var length = stream.Length;
if (length <= 0)
{
throw new IOException("Model file length is zero.");
}
if (length > int.MaxValue)
{
throw new IOException("Model file too large.");
}
var buffer = new byte[(int)length];
var totalRead = 0;
while (totalRead < buffer.Length)
{
var read = stream.Read(buffer, totalRead, buffer.Length - totalRead);
if (read == 0)
{
break;
}
totalRead += read;
}
if (totalRead != buffer.Length || stream.Length != length)
{
throw new IOException("Model file length changed during read.");
}
return buffer;
}
private static bool TryProcessMesh(
MdlFile mdl,
int lodIndex,
int meshIndex,
MeshStruct mesh,
MdlStructs.SubmeshStruct[] meshSubMeshes,
ModelDecimationSettings settings,
ModelDecimationAdvancedSettings tuning,
BodyCollisionData? bodyCollision,
out MeshStruct updatedMesh,
out MdlStructs.SubmeshStruct[] updatedSubMeshes,
out byte[][] vertexStreams,
out int[] indices,
out bool decimated,
MsLogger logger)
{
updatedMesh = mesh;
updatedSubMeshes = [];
vertexStreams = [[], [], []];
indices = [];
decimated = false;
if (mesh.VertexCount == 0 || mesh.IndexCount == 0)
{
return false;
}
if (meshSubMeshes.Length == 0)
{
return false;
}
var triangleCount = (int)(mesh.IndexCount / 3);
if (triangleCount < settings.TriangleThreshold)
{
return false;
}
if (!TryBuildVertexFormat(mdl.VertexDeclarations[meshIndex], out var format, out var reason))
{
logger.LogDebug("Mesh {MeshIndex} vertex format unsupported: {Reason}", meshIndex, reason);
return false;
}
if (!TryDecodeMeshData(mdl, lodIndex, mesh, format, meshSubMeshes, out var decoded, out var subMeshIndices, out var decodeReason))
{
logger.LogDebug("Mesh {MeshIndex} decode failed: {Reason}", meshIndex, decodeReason);
return false;
}
var targetTriangles = (int)Math.Floor(triangleCount * settings.TargetRatio);
if (targetTriangles < 1 || targetTriangles >= triangleCount)
{
logger.LogDebug(
"Mesh {MeshIndex} decimation target invalid ({Target} vs {Triangles})",
meshIndex,
targetTriangles,
triangleCount);
return false;
}
var collisionData = bodyCollision;
if (collisionData != null && IsBodyMesh(mdl, mesh))
{
collisionData = null;
}
if (!TryDecimateWithNanomesh(decoded, subMeshIndices, format, targetTriangles, tuning, collisionData, out var decimatedData, out var decimatedSubMeshIndices, out var decimationStats, out var decimationReason))
{
logger.LogDebug("Mesh {MeshIndex} decimation failed: {Reason}", meshIndex, decimationReason);
return false;
}
if (decimatedSubMeshIndices.Length != meshSubMeshes.Length)
{
logger.LogDebug("Mesh {MeshIndex} submesh count changed after decimation", meshIndex);
return false;
}
var decimatedTriangles = 0;
for (var subMeshIndex = 0; subMeshIndex < decimatedSubMeshIndices.Length; subMeshIndex++)
{
decimatedTriangles += decimatedSubMeshIndices[subMeshIndex].Length / 3;
}
if (decimatedTriangles <= 0 || decimatedTriangles >= triangleCount)
{
logger.LogDebug(
"Mesh {MeshIndex} decimation produced no reduction (before {Before}, after {After}, components {Components}, eligible {Eligible}, min {Min}, max {Max}, avg {Avg:0.##}, eval {Evaluated}, collapsed {Collapsed}, reject bone {RejectBone}, body {RejectBody}, topo {RejectTopo}, invert {RejectInvert} (deg {RejectDeg}, area {RejectArea}, flip {RejectFlip})",
meshIndex,
triangleCount,
decimatedTriangles,
decimationStats.TotalComponents,
decimationStats.EligibleComponents,
decimationStats.MinTriangles,
decimationStats.MaxTriangles,
decimationStats.AvgTriangles,
decimationStats.EvaluatedEdges,
decimationStats.CollapsedEdges,
decimationStats.RejectedBoneWeights,
decimationStats.RejectedBodyCollision,
decimationStats.RejectedTopology,
decimationStats.RejectedInversion,
decimationStats.RejectedDegenerate,
decimationStats.RejectedArea,
decimationStats.RejectedFlip);
return false;
}
if (!TryEncodeMeshData(decimatedData, decimatedSubMeshIndices, format, mesh, meshSubMeshes, settings.NormalizeTangents, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason))
{
logger.LogDebug("Mesh {MeshIndex} encode failed: {Reason}", meshIndex, encodeReason);
return false;
}
decimated = true;
return true;
}
private static bool TryDecimateWithNanomesh(
DecodedMeshData decoded,
int[][] subMeshIndices,
VertexFormat format,
int targetTriangles,
ModelDecimationAdvancedSettings tuning,
BodyCollisionData? bodyCollision,
out DecodedMeshData decimated,
out int[][] decimatedSubMeshIndices,
out ComponentStats componentStats,
out string? reason)
{
decimated = default!;
decimatedSubMeshIndices = [];
componentStats = default;
reason = null;
var totalTriangles = 0;
for (var i = 0; i < subMeshIndices.Length; i++)
{
totalTriangles += subMeshIndices[i].Length / 3;
}
if (totalTriangles <= 0)
{
reason = "No triangles to decimate.";
return false;
}
var targetRatio = Math.Clamp(targetTriangles / (float)totalTriangles, 0f, 1f);
var outputSubMeshes = new List<int>[subMeshIndices.Length];
for (var i = 0; i < outputSubMeshes.Length; i++)
{
outputSubMeshes[i] = new List<int>();
}
var positions = new List<Vector3d>();
var normals = format.HasNormals ? new List<Vector3>() : null;
var tangents = format.HasTangent1 ? new List<Vector4>() : null;
var tangents2 = format.HasTangent2 ? new List<Vector4>() : null;
var colors = format.HasColors ? new List<Vector4>() : null;
var boneWeights = format.HasSkinning ? new List<BoneWeight>() : null;
var positionWs = format.HasPositionW ? new List<float>() : null;
var normalWs = format.HasNormalW ? new List<float>() : null;
List<Vector2>[]? uvChannels = null;
if (format.UvChannelCount > 0)
{
uvChannels = new List<Vector2>[format.UvChannelCount];
for (var channel = 0; channel < format.UvChannelCount; channel++)
{
uvChannels[channel] = new List<Vector2>();
}
}
var componentCount = 0;
var eligibleCount = 0;
var minComponentTriangles = int.MaxValue;
var maxComponentTriangles = 0;
var totalComponentTriangles = 0;
var evaluatedEdges = 0;
var collapsedEdges = 0;
var rejectedBoneWeights = 0;
var rejectedTopology = 0;
var rejectedInversion = 0;
var rejectedDegenerate = 0;
var rejectedArea = 0;
var rejectedFlip = 0;
var rejectedBodyCollision = 0;
for (var subMeshIndex = 0; subMeshIndex < subMeshIndices.Length; subMeshIndex++)
{
var indices = subMeshIndices[subMeshIndex];
if (indices.Length == 0)
{
continue;
}
var components = BuildComponentsForSubMesh(indices);
foreach (var componentIndices in components)
{
if (componentIndices.Length == 0)
{
continue;
}
var componentTriangles = componentIndices.Length / 3;
if (componentTriangles == 0)
{
continue;
}
var componentTarget = ComputeComponentTarget(componentTriangles, targetRatio, tuning.MinComponentTriangles);
componentCount++;
totalComponentTriangles += componentTriangles;
minComponentTriangles = Math.Min(minComponentTriangles, componentTriangles);
maxComponentTriangles = Math.Max(maxComponentTriangles, componentTriangles);
if (componentTarget < componentTriangles)
{
eligibleCount++;
}
if (!TryBuildComponentDecoded(decoded, format, componentIndices, out var componentDecoded, out var componentLocalIndices, out reason))
{
return false;
}
DecodedMeshData componentDecimated = componentDecoded;
var componentDecimatedIndices = componentLocalIndices;
if (componentTarget < componentTriangles)
{
if (TryDecimateComponent(componentDecoded, format, componentLocalIndices, componentTarget, decoded.BlendWeightEncoding, tuning, bodyCollision, out var decimatedComponent, out var decimatedComponentIndices, out var decimatorStats, out _))
{
componentDecimated = decimatedComponent;
componentDecimatedIndices = decimatedComponentIndices;
evaluatedEdges += decimatorStats.EvaluatedEdges;
collapsedEdges += decimatorStats.CollapsedEdges;
rejectedBoneWeights += decimatorStats.RejectedBoneWeights;
rejectedTopology += decimatorStats.RejectedTopology;
rejectedInversion += decimatorStats.RejectedInversion;
rejectedDegenerate += decimatorStats.RejectedDegenerate;
rejectedArea += decimatorStats.RejectedArea;
rejectedFlip += decimatorStats.RejectedFlip;
rejectedBodyCollision += decimatorStats.RejectedBodyCollision;
}
}
if (!AppendComponentData(
componentDecimated,
componentDecimatedIndices,
format,
positions,
normals,
tangents,
tangents2,
colors,
boneWeights,
uvChannels,
positionWs,
normalWs,
outputSubMeshes[subMeshIndex],
out reason))
{
return false;
}
}
}
if (positions.Count > ushort.MaxValue)
{
reason = "Decimated mesh exceeds vertex limit.";
return false;
}
componentStats = BuildComponentStats(
componentCount,
eligibleCount,
minComponentTriangles,
maxComponentTriangles,
totalComponentTriangles,
evaluatedEdges,
collapsedEdges,
rejectedBoneWeights,
rejectedTopology,
rejectedInversion,
rejectedDegenerate,
rejectedArea,
rejectedFlip,
rejectedBodyCollision);
decimated = new DecodedMeshData(
positions.ToArray(),
normals?.ToArray(),
tangents?.ToArray(),
tangents2?.ToArray(),
colors?.ToArray(),
boneWeights?.ToArray(),
uvChannels?.Select(channel => channel.ToArray()).ToArray(),
positionWs?.ToArray(),
normalWs?.ToArray(),
decoded.BlendWeightEncoding);
decimatedSubMeshIndices = outputSubMeshes.Select(list => list.ToArray()).ToArray();
return true;
}
private static ComponentStats BuildComponentStats(
int componentCount,
int eligibleCount,
int minTriangles,
int maxTriangles,
int totalTriangles,
int evaluatedEdges,
int collapsedEdges,
int rejectedBoneWeights,
int rejectedTopology,
int rejectedInversion,
int rejectedDegenerate,
int rejectedArea,
int rejectedFlip,
int rejectedBodyCollision)
{
if (componentCount <= 0)
{
return new ComponentStats(0, 0, 0, 0, 0d, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
var average = totalTriangles / (double)componentCount;
return new ComponentStats(
componentCount,
eligibleCount,
minTriangles == int.MaxValue ? 0 : minTriangles,
maxTriangles,
average,
evaluatedEdges,
collapsedEdges,
rejectedBoneWeights,
rejectedTopology,
rejectedInversion,
rejectedDegenerate,
rejectedArea,
rejectedFlip,
rejectedBodyCollision);
}
private readonly record struct ComponentStats(
int TotalComponents,
int EligibleComponents,
int MinTriangles,
int MaxTriangles,
double AvgTriangles,
int EvaluatedEdges,
int CollapsedEdges,
int RejectedBoneWeights,
int RejectedTopology,
int RejectedInversion,
int RejectedDegenerate,
int RejectedArea,
int RejectedFlip,
int RejectedBodyCollision);
private static int ComputeComponentTarget(int componentTriangles, float targetRatio, int minComponentTriangles)
{
var minTriangles = Math.Max(1, minComponentTriangles);
if (componentTriangles <= minTriangles)
{
return componentTriangles;
}
var target = (int)MathF.Round(componentTriangles * targetRatio);
target = Math.Max(1, target);
return Math.Min(componentTriangles, Math.Max(minTriangles, target));
}
private static List<int[]> BuildComponentsForSubMesh(int[] indices)
{
var components = new List<int[]>();
if (indices.Length == 0)
{
return components;
}
var triangleCount = indices.Length / 3;
if (triangleCount <= 1)
{
components.Add(indices);
return components;
}
var parent = new int[triangleCount];
var rank = new byte[triangleCount];
for (var i = 0; i < triangleCount; i++)
{
parent[i] = i;
}
var vertexToTriangle = new Dictionary<int, int>();
for (var tri = 0; tri < triangleCount; tri++)
{
var baseIndex = tri * 3;
for (var v = 0; v < 3; v++)
{
var vertexIndex = indices[baseIndex + v];
if (vertexToTriangle.TryGetValue(vertexIndex, out var existing))
{
Union(parent, rank, tri, existing);
}
else
{
vertexToTriangle[vertexIndex] = tri;
}
}
}
var componentMap = new Dictionary<int, List<int>>();
for (var tri = 0; tri < triangleCount; tri++)
{
var root = Find(parent, tri);
if (!componentMap.TryGetValue(root, out var list))
{
list = [];
componentMap[root] = list;
}
list.Add(tri);
}
foreach (var component in componentMap.Values)
{
var slice = new int[component.Count * 3];
var cursor = 0;
foreach (var tri in component)
{
Array.Copy(indices, tri * 3, slice, cursor, 3);
cursor += 3;
}
components.Add(slice);
}
return components;
}
private static bool TryBuildComponentDecoded(
DecodedMeshData decoded,
VertexFormat format,
int[] componentIndices,
out DecodedMeshData componentDecoded,
out int[] componentLocalIndices,
out string? reason)
{
componentDecoded = default!;
componentLocalIndices = [];
reason = null;
if (componentIndices.Length == 0)
{
reason = "Component has no indices.";
return false;
}
var vertexMap = new Dictionary<int, int>();
var positions = new List<Vector3d>();
var normals = format.HasNormals ? new List<Vector3>() : null;
var tangents = format.HasTangent1 ? new List<Vector4>() : null;
var tangents2 = format.HasTangent2 ? new List<Vector4>() : null;
var colors = format.HasColors ? new List<Vector4>() : null;
var boneWeights = format.HasSkinning ? new List<BoneWeight>() : null;
var positionWs = format.HasPositionW ? new List<float>() : null;
var normalWs = format.HasNormalW ? new List<float>() : null;
List<Vector2>[]? uvChannels = null;
if (format.UvChannelCount > 0)
{
uvChannels = new List<Vector2>[format.UvChannelCount];
for (var channel = 0; channel < format.UvChannelCount; channel++)
{
uvChannels[channel] = new List<Vector2>();
}
}
componentLocalIndices = new int[componentIndices.Length];
for (var i = 0; i < componentIndices.Length; i++)
{
var globalIndex = componentIndices[i];
if (globalIndex < 0 || globalIndex >= decoded.Positions.Length)
{
reason = "Component vertex index out of bounds.";
return false;
}
if (!vertexMap.TryGetValue(globalIndex, out var localIndex))
{
localIndex = positions.Count;
vertexMap[globalIndex] = localIndex;
positions.Add(decoded.Positions[globalIndex]);
if (normals != null)
{
normals.Add(decoded.Normals != null ? decoded.Normals[globalIndex] : default);
}
if (tangents != null)
{
tangents.Add(decoded.Tangents != null ? decoded.Tangents[globalIndex] : default);
}
if (tangents2 != null)
{
tangents2.Add(decoded.Tangents2 != null ? decoded.Tangents2[globalIndex] : default);
}
if (colors != null)
{
colors.Add(decoded.Colors != null ? decoded.Colors[globalIndex] : default);
}
if (boneWeights != null)
{
boneWeights.Add(decoded.BoneWeights != null ? decoded.BoneWeights[globalIndex] : default);
}
if (positionWs != null)
{
positionWs.Add(decoded.PositionWs != null ? decoded.PositionWs[globalIndex] : 0f);
}
if (normalWs != null)
{
normalWs.Add(decoded.NormalWs != null ? decoded.NormalWs[globalIndex] : 0f);
}
if (uvChannels != null)
{
for (var channel = 0; channel < uvChannels.Length; channel++)
{
var source = decoded.UvChannels != null && channel < decoded.UvChannels.Length
? decoded.UvChannels[channel]
: null;
uvChannels[channel].Add(source != null ? source[globalIndex] : default);
}
}
}
componentLocalIndices[i] = localIndex;
}
componentDecoded = new DecodedMeshData(
positions.ToArray(),
normals?.ToArray(),
tangents?.ToArray(),
tangents2?.ToArray(),
colors?.ToArray(),
boneWeights?.ToArray(),
uvChannels?.Select(channel => channel.ToArray()).ToArray(),
positionWs?.ToArray(),
normalWs?.ToArray(),
decoded.BlendWeightEncoding);
return true;
}
private static bool TryDecimateComponent(
DecodedMeshData componentDecoded,
VertexFormat format,
int[] componentIndices,
int targetTriangles,
BlendWeightEncoding blendWeightEncoding,
ModelDecimationAdvancedSettings tuning,
BodyCollisionData? bodyCollision,
out DecodedMeshData decimated,
out int[] decimatedIndices,
out Nano.DecimationStats decimatorStats,
out string? reason)
{
decimated = default!;
decimatedIndices = [];
decimatorStats = default;
reason = null;
var componentTriangles = componentIndices.Length / 3;
var avgEdgeLength = ComputeAverageEdgeLength(componentDecoded.Positions, componentIndices);
bool RunDecimation(
float bodyCollisionDistanceFactor,
bool allowProtectedVertices,
bool expandProtectedVertices,
bool allowProtectedVerticesWhenRelaxed,
bool forceRelaxTopology,
bool blockUvSeamVertices,
float? uvSeamAngleCosOverride,
out DecodedMeshData runDecimated,
out int[] runDecimatedIndices,
out Nano.DecimationStats runDecimatorStats,
out string? runReason)
{
runDecimated = default!;
runDecimatedIndices = [];
runDecimatorStats = default;
runReason = null;
if (!TryBuildNanomeshMesh(componentDecoded, [componentIndices], format, out var sharedMesh, out runReason))
{
return false;
}
if (avgEdgeLength > 0f && tuning.MaxCollapseEdgeLengthFactor > 0f)
{
Nano.DecimateModifier.LimitCollapseEdgeLength = true;
Nano.DecimateModifier.MaxCollapseEdgeLength = avgEdgeLength * tuning.MaxCollapseEdgeLengthFactor;
}
else
{
Nano.DecimateModifier.LimitCollapseEdgeLength = false;
Nano.DecimateModifier.MaxCollapseEdgeLength = float.PositiveInfinity;
}
var relaxTopology = forceRelaxTopology;
var decimator = new Nano.DecimateModifier();
if (bodyCollision != null)
{
var threshold = MathF.Max(avgEdgeLength * bodyCollisionDistanceFactor + tuning.BodyCollisionProxyInflate, tuning.MinBodyCollisionDistance);
var bodyDistanceSq = bodyCollision.ComputeDistanceSq(componentDecoded.Positions, threshold);
if (bodyDistanceSq != null)
{
var thresholdSq = threshold * threshold;
var protectionThreshold = MathF.Max(threshold * tuning.BodyCollisionProtectionFactor, threshold);
var protectionThresholdSq = protectionThreshold * protectionThreshold;
var protectedDistanceSq = allowProtectedVertices
? bodyCollision.ComputeDistanceSq(componentDecoded.Positions, protectionThreshold)
: null;
var relaxedBodyGuard = forceRelaxTopology;
if (!forceRelaxTopology && IsNearBodyDominant(bodyDistanceSq, thresholdSq, componentDecoded.Positions.Length, tuning.BodyCollisionAdaptiveNearRatio))
{
threshold = MathF.Max(threshold * tuning.BodyCollisionAdaptiveRelaxFactor, tuning.MinBodyCollisionDistance);
thresholdSq = threshold * threshold;
relaxedBodyGuard = true;
relaxTopology = true;
protectionThreshold = MathF.Max(threshold * tuning.BodyCollisionProtectionFactor, threshold);
protectionThresholdSq = protectionThreshold * protectionThreshold;
if (allowProtectedVertices)
{
protectedDistanceSq = bodyCollision.ComputeDistanceSq(componentDecoded.Positions, protectionThreshold);
}
}
decimator.SetBodyCollision(bodyDistanceSq, thresholdSq, point => bodyCollision.DistanceSq(point, thresholdSq));
if (allowProtectedVertices && (!relaxedBodyGuard || allowProtectedVerticesWhenRelaxed))
{
var useExpandedProtection = expandProtectedVertices && !relaxTopology;
var protectedVertices = protectedDistanceSq != null
? BuildProtectedVertices(componentDecoded.Positions.Length, componentIndices, protectedDistanceSq, protectionThresholdSq, useExpandedProtection)
: null;
if (protectedVertices != null)
{
decimator.SetProtectedVertices(protectedVertices);
}
}
}
}
if (relaxTopology)
{
sharedMesh.attributeDefinitions = [new Nano.AttributeDefinition(Nano.AttributeType.Normals, 0d, 0)];
}
var connectedMesh = sharedMesh.ToConnectedMesh();
Nano.DecimateModifier.CollapseToEndpointsOnly = true;
var previousNormalSimilarity = Nano.DecimateModifier.NormalSimilarityThresholdDegrees;
var previousBoneWeightSimilarity = Nano.DecimateModifier.BoneWeightSimilarityThreshold;
var previousBodyPenetration = Nano.DecimateModifier.BodyCollisionPenetrationFactor;
var previousUvThreshold = Nano.DecimateModifier.UvSimilarityThreshold;
var previousAllowBoundary = Nano.DecimateModifier.AllowBoundaryCollapses;
var previousBlockUvSeamVertices = Nano.DecimateModifier.BlockUvSeamVertices;
var previousUvSeamAngleCos = Nano.DecimateModifier.UvSeamAngleCos;
try
{
Nano.DecimateModifier.NormalSimilarityThresholdDegrees = tuning.NormalSimilarityThresholdDegrees;
Nano.DecimateModifier.BoneWeightSimilarityThreshold = tuning.BoneWeightSimilarityThreshold;
Nano.DecimateModifier.BodyCollisionPenetrationFactor = tuning.BodyCollisionPenetrationFactor;
Nano.DecimateModifier.UvSimilarityThreshold = tuning.UvSimilarityThreshold;
Nano.DecimateModifier.AllowBoundaryCollapses = tuning.AllowBoundaryCollapses;
Nano.DecimateModifier.BlockUvSeamVertices = blockUvSeamVertices && tuning.BlockUvSeamVertices;
Nano.DecimateModifier.UvSeamAngleCos = tuning.UvSeamAngleCos;
if (relaxTopology)
{
Nano.DecimateModifier.UvSimilarityThreshold = tuning.BodyCollisionAdaptiveUvThreshold;
Nano.DecimateModifier.AllowBoundaryCollapses = false;
}
if (uvSeamAngleCosOverride.HasValue)
{
Nano.DecimateModifier.UvSeamAngleCos = uvSeamAngleCosOverride.Value;
}
decimator.Initialize(connectedMesh);
decimator.DecimateToPolycount(targetTriangles);
runDecimatorStats = decimator.GetStats();
}
finally
{
Nano.DecimateModifier.NormalSimilarityThresholdDegrees = previousNormalSimilarity;
Nano.DecimateModifier.BoneWeightSimilarityThreshold = previousBoneWeightSimilarity;
Nano.DecimateModifier.BodyCollisionPenetrationFactor = previousBodyPenetration;
Nano.DecimateModifier.UvSimilarityThreshold = previousUvThreshold;
Nano.DecimateModifier.AllowBoundaryCollapses = previousAllowBoundary;
Nano.DecimateModifier.BlockUvSeamVertices = previousBlockUvSeamVertices;
Nano.DecimateModifier.UvSeamAngleCos = previousUvSeamAngleCos;
}
var decimatedShared = connectedMesh.ToSharedMesh();
if (!TryConvertNanomeshMesh(decimatedShared, format, 1, blendWeightEncoding, out runDecimated, out var subMeshes, out runReason))
{
return false;
}
if (subMeshes.Length > 0)
{
runDecimatedIndices = subMeshes[0];
}
return true;
}
if (!RunDecimation(
tuning.BodyCollisionDistanceFactor,
allowProtectedVertices: true,
expandProtectedVertices: true,
allowProtectedVerticesWhenRelaxed: true,
forceRelaxTopology: false,
blockUvSeamVertices: true,
uvSeamAngleCosOverride: null,
out decimated,
out decimatedIndices,
out decimatorStats,
out reason))
{
return false;
}
if (decimatorStats.CollapsedEdges == 0 && targetTriangles < componentTriangles && bodyCollision != null)
{
if (RunDecimation(
tuning.BodyCollisionNoOpDistanceFactor,
allowProtectedVertices: true,
expandProtectedVertices: false,
allowProtectedVerticesWhenRelaxed: true,
forceRelaxTopology: true,
blockUvSeamVertices: false,
uvSeamAngleCosOverride: tuning.BodyCollisionNoOpUvSeamAngleCos,
out var fallbackDecimated,
out var fallbackDecimatedIndices,
out var fallbackStats,
out _))
{
var fallbackTriangles = fallbackDecimatedIndices.Length / 3;
if (fallbackStats.CollapsedEdges > 0 && fallbackTriangles > 0 && fallbackTriangles < componentTriangles)
{
decimated = fallbackDecimated;
decimatedIndices = fallbackDecimatedIndices;
decimatorStats = fallbackStats;
}
}
}
return true;
}
private static float ComputeAverageEdgeLength(Vector3d[] positions, int[] indices)
{
if (positions.Length == 0 || indices.Length < 3)
{
return 0f;
}
double sum = 0d;
int count = 0;
for (var i = 0; i + 2 < indices.Length; i += 3)
{
var i0 = indices[i];
var i1 = indices[i + 1];
var i2 = indices[i + 2];
if ((uint)i0 >= positions.Length || (uint)i1 >= positions.Length || (uint)i2 >= positions.Length)
{
continue;
}
sum += Vector3d.Distance(positions[i0], positions[i1]);
sum += Vector3d.Distance(positions[i1], positions[i2]);
sum += Vector3d.Distance(positions[i2], positions[i0]);
count += 3;
}
return count > 0 ? (float)(sum / count) : 0f;
}
private static bool[]? BuildProtectedVertices(int vertexCount, int[] indices, float[] distanceSq, float thresholdSq, bool expand)
{
if (vertexCount <= 0 || distanceSq.Length == 0)
{
return null;
}
var seed = new bool[vertexCount];
var seedCount = 0;
var limit = Math.Min(vertexCount, distanceSq.Length);
for (var i = 0; i < limit; i++)
{
if (distanceSq[i] <= thresholdSq)
{
seed[i] = true;
seedCount++;
}
}
if (seedCount == 0)
{
return null;
}
if (!expand || indices.Length < 3)
{
return seed;
}
var expanded = (bool[])seed.Clone();
for (var i = 0; i + 2 < indices.Length; i += 3)
{
var a = indices[i];
var b = indices[i + 1];
var c = indices[i + 2];
if ((uint)a >= vertexCount || (uint)b >= vertexCount || (uint)c >= vertexCount)
{
continue;
}
if (seed[a] || seed[b] || seed[c])
{
expanded[a] = true;
expanded[b] = true;
expanded[c] = true;
}
}
return expanded;
}
private static bool IsNearBodyDominant(float[] distanceSq, float thresholdSq, int vertexCount, float adaptiveNearRatio)
{
if (vertexCount <= 0 || distanceSq.Length == 0 || thresholdSq <= 0f)
{
return false;
}
var limit = Math.Min(vertexCount, distanceSq.Length);
var nearCount = 0;
for (var i = 0; i < limit; i++)
{
if (distanceSq[i] <= thresholdSq)
{
nearCount++;
}
}
return nearCount >= limit * adaptiveNearRatio;
}
private sealed record PreprocessedMeshOutput(
MeshStruct Mesh,
MdlStructs.SubmeshStruct[] SubMeshes,
byte[][] VertexStreams,
int[] Indices,
bool Decimated);
private static bool TryBuildBodyCollisionData(
MdlFile mdl,
int lodIndex,
int lodMeshStart,
int lodMeshEnd,
ModelDecimationSettings settings,
ModelDecimationAdvancedSettings tuning,
out BodyCollisionData? bodyCollision,
out Dictionary<int, PreprocessedMeshOutput> bodyMeshOverrides,
MsLogger logger)
{
bodyCollision = null;
bodyMeshOverrides = [];
var meshCount = Math.Max(0, lodMeshEnd - lodMeshStart);
logger.LogDebug("Body collision: scanning {MeshCount} meshes, {MaterialCount} materials", meshCount, mdl.Materials.Length);
if (mdl.Materials.Length == 0)
{
logger.LogDebug("Body collision: no materials found, skipping body collision.");
return false;
}
var materialList = string.Join(", ", mdl.Materials);
logger.LogDebug("Body collision: model materials = {Materials}", materialList);
var proxyTargetRatio = Math.Clamp(Math.Max(settings.TargetRatio, tuning.BodyProxyTargetRatioMin), 0d, 1d);
var bodyPositions = new List<Vector3d>();
var bodyIndices = new List<int>();
var foundBody = false;
for (var meshIndex = lodMeshStart; meshIndex < lodMeshEnd; meshIndex++)
{
var mesh = mdl.Meshes[meshIndex];
var material = mesh.MaterialIndex < mdl.Materials.Length
? mdl.Materials[mesh.MaterialIndex]
: "(missing material)";
var isBody = IsBodyMaterial(material);
logger.LogDebug("Body collision: mesh {MeshIndex} material {Material} body {IsBody}", meshIndex, material, isBody);
if (!isBody)
{
continue;
}
foundBody = true;
var meshSubMeshes = mdl.SubMeshes
.Skip(mesh.SubMeshIndex)
.Take(mesh.SubMeshCount)
.ToArray();
if (!TryBuildVertexFormat(mdl.VertexDeclarations[meshIndex], out var format, out var formatReason))
{
logger.LogDebug("Body mesh {MeshIndex} vertex format unsupported: {Reason}", meshIndex, formatReason);
continue;
}
if (!TryDecodeMeshData(mdl, lodIndex, mesh, format, meshSubMeshes, out var decoded, out var subMeshIndices, out var decodeReason))
{
logger.LogDebug("Body mesh {MeshIndex} decode failed: {Reason}", meshIndex, decodeReason);
continue;
}
var triangleCount = (int)(mesh.IndexCount / 3);
var updatedMesh = mesh;
var updatedSubMeshes = CopySubMeshes(meshSubMeshes, 0, mesh.StartIndex);
var vertexStreams = CopyVertexStreams(mdl, lodIndex, mesh);
var indices = ReadIndices(mdl, lodIndex, mesh);
var decimated = false;
var collisionDecoded = decoded;
var collisionSubMeshIndices = subMeshIndices;
if (triangleCount >= settings.TriangleThreshold)
{
var targetTriangles = (int)Math.Floor(triangleCount * proxyTargetRatio);
if (targetTriangles >= 1 && targetTriangles < triangleCount)
{
if (TryDecimateWithNanomesh(decoded, subMeshIndices, format, targetTriangles, tuning, null, out var decimatedData, out var decimatedSubMeshIndices, out _, out var decimationReason))
{
if (TryEncodeMeshData(decimatedData, decimatedSubMeshIndices, format, mesh, meshSubMeshes, settings.NormalizeTangents, out updatedMesh, out updatedSubMeshes, out vertexStreams, out indices, out var encodeReason))
{
decimated = true;
collisionDecoded = decimatedData;
collisionSubMeshIndices = decimatedSubMeshIndices;
}
else
{
logger.LogDebug("Body mesh {MeshIndex} encode failed: {Reason}", meshIndex, encodeReason);
}
}
else
{
logger.LogDebug("Body mesh {MeshIndex} decimation failed: {Reason}", meshIndex, decimationReason);
}
}
}
bodyMeshOverrides[meshIndex] = new PreprocessedMeshOutput(updatedMesh, updatedSubMeshes, vertexStreams, indices, decimated);
var baseIndex = bodyPositions.Count;
bodyPositions.AddRange(collisionDecoded.Positions);
foreach (var subMesh in collisionSubMeshIndices)
{
for (var i = 0; i < subMesh.Length; i++)
{
bodyIndices.Add(subMesh[i] + baseIndex);
}
}
}
if (!foundBody)
{
logger.LogDebug("Body collision: no body meshes matched filter.");
return false;
}
if (bodyPositions.Count == 0 || bodyIndices.Count == 0)
{
logger.LogDebug("Body collision enabled but no body vertices were collected.");
return false;
}
var positionArray = bodyPositions.ToArray();
var indexArray = bodyIndices.ToArray();
var avgEdgeLength = ComputeAverageEdgeLength(positionArray, indexArray);
var cellSize = MathF.Max(avgEdgeLength, tuning.MinBodyCollisionCellSize);
bodyCollision = new BodyCollisionData(positionArray, indexArray, cellSize, tuning.MinBodyCollisionCellSize);
return true;
}
private static bool IsBodyMesh(MdlFile mdl, MeshStruct mesh)
{
if (mesh.MaterialIndex >= mdl.Materials.Length)
{
return false;
}
return ModelDecimationFilters.IsBodyMaterial(mdl.Materials[mesh.MaterialIndex]);
}
private static bool IsBodyMaterial(string materialPath)
=> ModelDecimationFilters.IsBodyMaterial(materialPath);
private sealed class BodyCollisionData
{
private readonly Vector3d[] _positions;
private readonly BodyTriangle[] _triangles;
private readonly Dictionary<CellKey, List<int>> _triangleCells;
private readonly float _cellSize;
private readonly float _cellSizeInv;
public BodyCollisionData(Vector3d[] positions, int[] indices, float cellSize, float minCellSize)
{
_positions = positions;
_cellSize = cellSize > 0f ? cellSize : minCellSize;
_cellSizeInv = 1f / _cellSize;
var triangles = new List<BodyTriangle>();
for (var i = 0; i + 2 < indices.Length; i += 3)
{
var a = indices[i];
var b = indices[i + 1];
var c = indices[i + 2];
if ((uint)a >= _positions.Length || (uint)b >= _positions.Length || (uint)c >= _positions.Length)
{
continue;
}
var p0 = _positions[a];
var p1 = _positions[b];
var p2 = _positions[c];
var min = Vector3d.Min(p0, Vector3d.Min(p1, p2));
var max = Vector3d.Max(p0, Vector3d.Max(p1, p2));
triangles.Add(new BodyTriangle(a, b, c, min, max));
}
_triangles = triangles.ToArray();
_triangleCells = new Dictionary<CellKey, List<int>>();
for (var triIndex = 0; triIndex < _triangles.Length; triIndex++)
{
var tri = _triangles[triIndex];
var minCell = ToCell(tri.Min);
var maxCell = ToCell(tri.Max);
for (var x = minCell.X; x <= maxCell.X; x++)
{
for (var y = minCell.Y; y <= maxCell.Y; y++)
{
for (var z = minCell.Z; z <= maxCell.Z; z++)
{
var key = new CellKey(x, y, z);
if (!_triangleCells.TryGetValue(key, out var list))
{
list = [];
_triangleCells[key] = list;
}
list.Add(triIndex);
}
}
}
}
}
public float[]? ComputeDistanceSq(Vector3d[] queryPositions, float maxDistance)
{
if (_positions.Length == 0 || queryPositions.Length == 0 || maxDistance <= 0f || _triangles.Length == 0 || _triangleCells.Count == 0)
{
return null;
}
var result = new float[queryPositions.Length];
var maxDistanceSq = maxDistance * maxDistance;
var radius = Math.Max(1, (int)MathF.Ceiling(maxDistance / _cellSize));
for (var i = 0; i < queryPositions.Length; i++)
{
var cell = ToCell(queryPositions[i]);
double minSq = double.PositiveInfinity;
var found = false;
for (var x = -radius; x <= radius && !found; x++)
{
for (var y = -radius; y <= radius && !found; y++)
{
for (var z = -radius; z <= radius; z++)
{
var key = new CellKey(cell.X + x, cell.Y + y, cell.Z + z);
if (!_triangleCells.TryGetValue(key, out var list))
{
continue;
}
for (var idx = 0; idx < list.Count; idx++)
{
var tri = _triangles[list[idx]];
var sq = PointTriangleDistanceSq(queryPositions[i], _positions[tri.A], _positions[tri.B], _positions[tri.C]);
if (sq < minSq)
{
minSq = sq;
}
if (minSq <= maxDistanceSq)
{
found = true;
break;
}
}
}
}
}
result[i] = minSq < double.PositiveInfinity ? (float)minSq : float.PositiveInfinity;
}
return result;
}
public float DistanceSq(in Vector3d point, float maxDistanceSq)
{
if (_positions.Length == 0 || _triangles.Length == 0 || _triangleCells.Count == 0)
{
return float.PositiveInfinity;
}
if (maxDistanceSq <= 0f)
{
return float.PositiveInfinity;
}
var maxDistance = MathF.Sqrt(maxDistanceSq);
var radius = Math.Max(1, (int)MathF.Ceiling(maxDistance / _cellSize));
var cell = ToCell(point);
double minSq = double.PositiveInfinity;
for (var x = -radius; x <= radius; x++)
{
for (var y = -radius; y <= radius; y++)
{
for (var z = -radius; z <= radius; z++)
{
var key = new CellKey(cell.X + x, cell.Y + y, cell.Z + z);
if (!_triangleCells.TryGetValue(key, out var list))
{
continue;
}
for (var idx = 0; idx < list.Count; idx++)
{
var tri = _triangles[list[idx]];
var sq = PointTriangleDistanceSq(point, _positions[tri.A], _positions[tri.B], _positions[tri.C]);
if (sq < minSq)
{
minSq = sq;
}
if (minSq <= maxDistanceSq)
{
return (float)minSq;
}
}
}
}
}
return minSq < double.PositiveInfinity ? (float)minSq : float.PositiveInfinity;
}
private CellKey ToCell(in Vector3d position)
=> new(
(int)Math.Floor(position.x * _cellSizeInv),
(int)Math.Floor(position.y * _cellSizeInv),
(int)Math.Floor(position.z * _cellSizeInv));
}
private readonly record struct BodyTriangle(int A, int B, int C, Vector3d Min, Vector3d Max);
private readonly record struct CellKey(int X, int Y, int Z);
private static double PointTriangleDistanceSq(in Vector3d p, in Vector3d a, in Vector3d b, in Vector3d c)
{
var ab = b - a;
var ac = c - a;
var ap = p - a;
var d1 = Vector3d.Dot(ab, ap);
var d2 = Vector3d.Dot(ac, ap);
if (d1 <= 0d && d2 <= 0d)
{
return (p - a).LengthSquared;
}
var bp = p - b;
var d3 = Vector3d.Dot(ab, bp);
var d4 = Vector3d.Dot(ac, bp);
if (d3 >= 0d && d4 <= d3)
{
return (p - b).LengthSquared;
}
var vc = d1 * d4 - d3 * d2;
if (vc <= 0d && d1 >= 0d && d3 <= 0d)
{
var v = d1 / (d1 - d3);
var proj = a + ab * v;
return (p - proj).LengthSquared;
}
var cp = p - c;
var d5 = Vector3d.Dot(ab, cp);
var d6 = Vector3d.Dot(ac, cp);
if (d6 >= 0d && d5 <= d6)
{
return (p - c).LengthSquared;
}
var vb = d5 * d2 - d1 * d6;
if (vb <= 0d && d2 >= 0d && d6 <= 0d)
{
var w = d2 / (d2 - d6);
var proj = a + ac * w;
return (p - proj).LengthSquared;
}
var va = d3 * d6 - d5 * d4;
if (va <= 0d && (d4 - d3) >= 0d && (d5 - d6) >= 0d)
{
var w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
var proj = b + (c - b) * w;
return (p - proj).LengthSquared;
}
var denom = 1d / (va + vb + vc);
var v2 = vb * denom;
var w2 = vc * denom;
var projPoint = a + ab * v2 + ac * w2;
return (p - projPoint).LengthSquared;
}
private static bool AppendComponentData(
DecodedMeshData component,
int[] componentIndices,
VertexFormat format,
List<Vector3d> positions,
List<Vector3>? normals,
List<Vector4>? tangents,
List<Vector4>? tangents2,
List<Vector4>? colors,
List<BoneWeight>? boneWeights,
List<Vector2>[]? uvChannels,
List<float>? positionWs,
List<float>? normalWs,
List<int> outputIndices,
out string? reason)
{
reason = null;
if (component.Positions.Length == 0 || componentIndices.Length == 0)
{
return true;
}
var baseIndex = positions.Count;
positions.AddRange(component.Positions);
if (normals != null && component.Normals != null)
{
normals.AddRange(component.Normals);
}
if (tangents != null && component.Tangents != null)
{
tangents.AddRange(component.Tangents);
}
if (tangents2 != null && component.Tangents2 != null)
{
tangents2.AddRange(component.Tangents2);
}
if (colors != null && component.Colors != null)
{
colors.AddRange(component.Colors);
}
if (boneWeights != null && component.BoneWeights != null)
{
boneWeights.AddRange(component.BoneWeights);
}
if (positionWs != null && component.PositionWs != null)
{
positionWs.AddRange(component.PositionWs);
}
if (normalWs != null && component.NormalWs != null)
{
normalWs.AddRange(component.NormalWs);
}
if (uvChannels != null && component.UvChannels != null)
{
if (uvChannels.Length != component.UvChannels.Length)
{
reason = "UV channel mismatch while merging components.";
return false;
}
for (var channel = 0; channel < uvChannels.Length; channel++)
{
uvChannels[channel].AddRange(component.UvChannels[channel]);
}
}
for (var i = 0; i < componentIndices.Length; i++)
{
outputIndices.Add(componentIndices[i] + baseIndex);
}
return true;
}
private static int Find(int[] parent, int value)
{
var root = value;
while (parent[root] != root)
{
root = parent[root];
}
while (parent[value] != value)
{
var next = parent[value];
parent[value] = root;
value = next;
}
return root;
}
private static void Union(int[] parent, byte[] rank, int a, int b)
{
var rootA = Find(parent, a);
var rootB = Find(parent, b);
if (rootA == rootB)
{
return;
}
if (rank[rootA] < rank[rootB])
{
parent[rootA] = rootB;
return;
}
parent[rootB] = rootA;
if (rank[rootA] == rank[rootB])
{
rank[rootA]++;
}
}
private static bool TryBuildNanomeshMesh(
DecodedMeshData decoded,
int[][] subMeshIndices,
VertexFormat format,
out Nano.SharedMesh sharedMesh,
out string? reason)
{
sharedMesh = default!;
reason = null;
var vertexCount = decoded.Positions.Length;
if (vertexCount == 0)
{
reason = "No vertices to decimate.";
return false;
}
if (subMeshIndices.Length == 0)
{
reason = "No submesh indices.";
return false;
}
var positions = decoded.Positions;
var totalIndexCount = 0;
for (var i = 0; i < subMeshIndices.Length; i++)
{
totalIndexCount += subMeshIndices[i].Length;
}
var triangles = new int[totalIndexCount];
var groups = new Nano.Group[subMeshIndices.Length];
var cursor = 0;
for (var i = 0; i < subMeshIndices.Length; i++)
{
var subMesh = subMeshIndices[i];
if (subMesh.Length > 0)
{
Array.Copy(subMesh, 0, triangles, cursor, subMesh.Length);
}
groups[i] = new Nano.Group { firstIndex = cursor, indexCount = subMesh.Length };
cursor += subMesh.Length;
}
var flags = BuildFfxivAttributeFlags(format);
var attributes = new Nano.MetaAttributeList<Nano.FfxivVertexAttribute>(vertexCount);
for (var i = 0; i < vertexCount; i++)
{
var attr = new Nano.FfxivVertexAttribute(
flags,
format.HasNormals && decoded.Normals != null ? decoded.Normals[i] : default,
format.HasTangent1 && decoded.Tangents != null ? decoded.Tangents[i] : default,
format.HasTangent2 && decoded.Tangents2 != null ? decoded.Tangents2[i] : default,
format.UvChannelCount > 0 && decoded.UvChannels != null ? decoded.UvChannels[0][i] : default,
format.UvChannelCount > 1 && decoded.UvChannels != null ? decoded.UvChannels[1][i] : default,
format.UvChannelCount > 2 && decoded.UvChannels != null ? decoded.UvChannels[2][i] : default,
format.UvChannelCount > 3 && decoded.UvChannels != null ? decoded.UvChannels[3][i] : default,
format.HasColors && decoded.Colors != null ? decoded.Colors[i] : default,
format.HasSkinning && decoded.BoneWeights != null ? decoded.BoneWeights[i] : default,
format.HasPositionW && decoded.PositionWs != null ? decoded.PositionWs[i] : 0f,
format.HasNormalW && decoded.NormalWs != null ? decoded.NormalWs[i] : 0f);
attributes[i] = new Nano.MetaAttribute<Nano.FfxivVertexAttribute>(attr);
}
sharedMesh = new Nano.SharedMesh
{
positions = positions,
triangles = triangles,
groups = groups,
attributes = attributes,
attributeDefinitions = [new Nano.AttributeDefinition(Nano.AttributeType.Normals, Nano.ConnectedMesh.EdgeBorderPenalty, 0)],
};
return true;
}
private static bool TryConvertNanomeshMesh(
Nano.SharedMesh decimatedShared,
VertexFormat format,
int expectedSubMeshCount,
BlendWeightEncoding blendWeightEncoding,
out DecodedMeshData decimated,
out int[][] decimatedSubMeshIndices,
out string? reason)
{
decimated = default!;
decimatedSubMeshIndices = [];
reason = null;
if (decimatedShared.triangles == null || decimatedShared.triangles.Length == 0)
{
reason = "No triangles after decimation.";
return false;
}
var groups = decimatedShared.groups;
var triangles = decimatedShared.triangles;
int[][] subMeshIndices;
if (groups != null && groups.Length == expectedSubMeshCount)
{
subMeshIndices = new int[groups.Length][];
for (var i = 0; i < groups.Length; i++)
{
var group = groups[i];
if (group.firstIndex < 0 || group.indexCount < 0 || group.firstIndex + group.indexCount > triangles.Length)
{
reason = "Invalid submesh group range after decimation.";
return false;
}
var slice = new int[group.indexCount];
if (group.indexCount > 0)
{
Array.Copy(triangles, group.firstIndex, slice, 0, group.indexCount);
}
subMeshIndices[i] = slice;
}
}
else if (expectedSubMeshCount == 1)
{
subMeshIndices = [triangles];
}
else
{
reason = "Submesh group count mismatch after decimation.";
return false;
}
var vertexCount = decimatedShared.positions.Length;
var positions = decimatedShared.positions;
var attrList = decimatedShared.attributes as Nano.MetaAttributeList<Nano.FfxivVertexAttribute>;
if (attrList == null)
{
reason = "Missing vertex attributes after decimation.";
return false;
}
Vector3[]? normals = format.HasNormals ? new Vector3[vertexCount] : null;
Vector4[]? tangents = format.HasTangent1 ? new Vector4[vertexCount] : null;
Vector4[]? tangents2 = format.HasTangent2 ? new Vector4[vertexCount] : null;
Vector4[]? colors = format.HasColors ? new Vector4[vertexCount] : null;
BoneWeight[]? boneWeights = format.HasSkinning ? new BoneWeight[vertexCount] : null;
float[]? positionWs = format.HasPositionW ? new float[vertexCount] : null;
float[]? normalWs = format.HasNormalW ? new float[vertexCount] : null;
Vector2[][]? uvChannels = null;
if (format.UvChannelCount > 0)
{
uvChannels = new Vector2[format.UvChannelCount][];
for (var channel = 0; channel < format.UvChannelCount; channel++)
{
uvChannels[channel] = new Vector2[vertexCount];
}
}
for (var i = 0; i < vertexCount; i++)
{
var attr = (Nano.MetaAttribute<Nano.FfxivVertexAttribute>)attrList[i];
var data = attr.attr0;
if (normals != null)
{
normals[i] = data.normal;
}
if (tangents != null)
{
tangents[i] = data.tangent1;
}
if (tangents2 != null)
{
tangents2[i] = data.tangent2;
}
if (colors != null)
{
colors[i] = data.color;
}
if (boneWeights != null)
{
boneWeights[i] = data.boneWeight;
}
if (positionWs != null)
{
positionWs[i] = data.positionW;
}
if (normalWs != null)
{
normalWs[i] = data.normalW;
}
if (uvChannels != null)
{
if (uvChannels.Length > 0)
{
uvChannels[0][i] = data.uv0;
}
if (uvChannels.Length > 1)
{
uvChannels[1][i] = data.uv1;
}
if (uvChannels.Length > 2)
{
uvChannels[2][i] = data.uv2;
}
if (uvChannels.Length > 3)
{
uvChannels[3][i] = data.uv3;
}
}
}
decimated = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
decimatedSubMeshIndices = subMeshIndices;
return true;
}
private static Nano.FfxivAttributeFlags BuildFfxivAttributeFlags(VertexFormat format)
{
var flags = Nano.FfxivAttributeFlags.None;
if (format.HasNormals)
{
flags |= Nano.FfxivAttributeFlags.Normal;
}
if (format.HasTangent1)
{
flags |= Nano.FfxivAttributeFlags.Tangent1;
}
if (format.HasTangent2)
{
flags |= Nano.FfxivAttributeFlags.Tangent2;
}
if (format.HasColors)
{
flags |= Nano.FfxivAttributeFlags.Color;
}
if (format.HasSkinning)
{
flags |= Nano.FfxivAttributeFlags.BoneWeights;
}
if (format.HasPositionW)
{
flags |= Nano.FfxivAttributeFlags.PositionW;
}
if (format.HasNormalW)
{
flags |= Nano.FfxivAttributeFlags.NormalW;
}
if (format.UvChannelCount > 0)
{
flags |= Nano.FfxivAttributeFlags.Uv0;
}
if (format.UvChannelCount > 1)
{
flags |= Nano.FfxivAttributeFlags.Uv1;
}
if (format.UvChannelCount > 2)
{
flags |= Nano.FfxivAttributeFlags.Uv2;
}
if (format.UvChannelCount > 3)
{
flags |= Nano.FfxivAttributeFlags.Uv3;
}
return flags;
}
private static bool TryDecodeMeshData(
MdlFile mdl,
int lodIndex,
MeshStruct mesh,
VertexFormat format,
MdlStructs.SubmeshStruct[] meshSubMeshes,
out DecodedMeshData decoded,
out int[][] subMeshIndices,
out string? reason)
{
decoded = default!;
subMeshIndices = [];
reason = null;
if (!TryBuildSubMeshIndices(mdl, lodIndex, mesh, meshSubMeshes, out subMeshIndices, out reason))
{
return false;
}
var vertexCount = mesh.VertexCount;
var positions = new Vector3d[vertexCount];
Vector3[]? normals = format.HasNormals ? new Vector3[vertexCount] : null;
Vector4[]? tangents = format.HasTangent1 ? new Vector4[vertexCount] : null;
Vector4[]? tangents2 = format.HasTangent2 ? new Vector4[vertexCount] : null;
Vector4[]? colors = format.HasColors ? new Vector4[vertexCount] : null;
BoneWeight[]? boneWeights = format.HasSkinning ? new BoneWeight[vertexCount] : null;
float[]? positionWs = format.HasPositionW ? new float[vertexCount] : null;
float[]? normalWs = format.HasNormalW ? new float[vertexCount] : null;
Vector2[][]? uvChannels = null;
if (format.UvChannelCount > 0)
{
uvChannels = new Vector2[format.UvChannelCount][];
for (var channel = 0; channel < format.UvChannelCount; channel++)
{
uvChannels[channel] = new Vector2[vertexCount];
}
}
var blendWeightEncoding = DetectBlendWeightEncoding(mdl, lodIndex, mesh, format);
var streams = new BinaryReader[MaxStreams];
for (var streamIndex = 0; streamIndex < MaxStreams; streamIndex++)
{
streams[streamIndex] = new BinaryReader(new MemoryStream(mdl.RemainingData));
streams[streamIndex].BaseStream.Position = mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(streamIndex);
}
var uvLookup = format.UvElements.ToDictionary(static element => ElementKey.From(element.Element), static element => element);
for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
{
int[]? indices = null;
float[]? weights = null;
foreach (var element in format.SortedElements)
{
var usage = (MdlFile.VertexUsage)element.Usage;
var type = (MdlFile.VertexType)element.Type;
var stream = streams[element.Stream];
switch (usage)
{
case MdlFile.VertexUsage.Position:
if (type == MdlFile.VertexType.Single4 && positionWs != null)
{
positions[vertexIndex] = ReadPositionWithW(stream, out positionWs[vertexIndex]);
}
else
{
positions[vertexIndex] = ReadPosition(type, stream);
}
break;
case MdlFile.VertexUsage.Normal when normals != null:
if (type == MdlFile.VertexType.Single4 && normalWs != null)
{
normals[vertexIndex] = ReadNormalWithW(stream, out normalWs[vertexIndex]);
}
else
{
normals[vertexIndex] = ReadNormal(type, stream);
}
break;
case MdlFile.VertexUsage.Tangent1 when tangents != null:
tangents[vertexIndex] = ReadTangent(type, stream);
break;
case MdlFile.VertexUsage.Tangent2 when tangents2 != null:
tangents2[vertexIndex] = ReadTangent(type, stream);
break;
case MdlFile.VertexUsage.Color when colors != null:
colors[vertexIndex] = ReadColor(type, stream);
break;
case MdlFile.VertexUsage.BlendIndices:
indices = ReadIndices(type, stream);
break;
case MdlFile.VertexUsage.BlendWeights:
weights = ReadWeights(type, stream, blendWeightEncoding);
break;
case MdlFile.VertexUsage.UV when uvChannels != null:
if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement))
{
reason = "UV mapping missing.";
return false;
}
ReadUv(type, stream, uvElement, uvChannels, vertexIndex);
break;
default:
if (usage == MdlFile.VertexUsage.Normal || usage == MdlFile.VertexUsage.Tangent1
|| usage == MdlFile.VertexUsage.Tangent2
|| usage == MdlFile.VertexUsage.Color)
{
_ = ReadAndDiscard(type, stream);
}
break;
}
}
if (boneWeights != null)
{
if (indices == null || weights == null || indices.Length != 4 || weights.Length != 4)
{
reason = "Missing or invalid skinning data.";
return false;
}
boneWeights[vertexIndex] = new BoneWeight(indices[0], indices[1], indices[2], indices[3], weights[0], weights[1], weights[2], weights[3]);
}
}
decoded = new DecodedMeshData(positions, normals, tangents, tangents2, colors, boneWeights, uvChannels, positionWs, normalWs, blendWeightEncoding);
return true;
}
private static bool TryEncodeMeshData(
DecodedMeshData decimated,
int[][] decimatedSubMeshIndices,
VertexFormat format,
MeshStruct originalMesh,
MdlStructs.SubmeshStruct[] originalSubMeshes,
bool normalizeTangents,
out MeshStruct updatedMesh,
out MdlStructs.SubmeshStruct[] updatedSubMeshes,
out byte[][] vertexStreams,
out int[] indices,
out string? reason)
{
updatedMesh = originalMesh;
updatedSubMeshes = [];
vertexStreams = [[], [], []];
indices = [];
reason = null;
if (decimatedSubMeshIndices.Length != originalSubMeshes.Length)
{
reason = "Decimated submesh count mismatch.";
return false;
}
var vertexCount = decimated.Positions.Length;
if (vertexCount > ushort.MaxValue)
{
reason = "Vertex count exceeds ushort range.";
return false;
}
var normals = decimated.Normals;
var tangents = decimated.Tangents;
var tangents2 = decimated.Tangents2;
var colors = decimated.Colors;
var boneWeights = decimated.BoneWeights;
var positionWs = decimated.PositionWs;
var normalWs = decimated.NormalWs;
if (format.HasNormals && normals == null)
{
reason = "Missing normals after decimation.";
return false;
}
if (format.HasTangent1 && tangents == null)
{
reason = "Missing tangent1 after decimation.";
return false;
}
if (format.HasTangent2 && tangents2 == null)
{
reason = "Missing tangent2 after decimation.";
return false;
}
if (normalizeTangents)
{
NormalizeTangents(tangents, clampW: true);
NormalizeTangents(tangents2, clampW: true);
}
if (format.HasColors && colors == null)
{
reason = "Missing colors after decimation.";
return false;
}
if (format.HasSkinning && boneWeights == null)
{
reason = "Missing bone weights after decimation.";
return false;
}
if (format.HasPositionW && positionWs == null)
{
reason = "Missing position W after decimation.";
return false;
}
if (format.HasNormalW && normalWs == null)
{
reason = "Missing normal W after decimation.";
return false;
}
var uvChannels = Array.Empty<Vector2[]>();
if (format.UvChannelCount > 0)
{
if (decimated.UvChannels == null || decimated.UvChannels.Length < format.UvChannelCount)
{
reason = "Missing UV channels after decimation.";
return false;
}
uvChannels = decimated.UvChannels;
}
var streamBuffers = new byte[MaxStreams][];
for (var stream = 0; stream < MaxStreams; stream++)
{
var stride = originalMesh.VertexBufferStride(stream);
if (stride == 0 || vertexCount == 0)
{
streamBuffers[stream] = [];
continue;
}
streamBuffers[stream] = new byte[stride * vertexCount];
}
var uvLookup = format.UvElements.ToDictionary(static element => ElementKey.From(element.Element), static element => element);
foreach (var element in format.SortedElements)
{
var stride = originalMesh.VertexBufferStride(element.Stream);
if (stride == 0)
{
continue;
}
var elementSize = GetElementSize((MdlFile.VertexType)element.Type);
if (element.Offset + elementSize > stride)
{
reason = "Vertex element stride overflow.";
return false;
}
}
for (var vertexIndex = 0; vertexIndex < vertexCount; vertexIndex++)
{
foreach (var element in format.SortedElements)
{
var usage = (MdlFile.VertexUsage)element.Usage;
var type = (MdlFile.VertexType)element.Type;
var stream = element.Stream;
var stride = originalMesh.VertexBufferStride(stream);
if (stride == 0)
{
continue;
}
var baseOffset = vertexIndex * stride + element.Offset;
var target = streamBuffers[stream].AsSpan(baseOffset, GetElementSize(type));
switch (usage)
{
case MdlFile.VertexUsage.Position:
WritePosition(type, decimated.Positions[vertexIndex], target, positionWs != null ? positionWs[vertexIndex] : null);
break;
case MdlFile.VertexUsage.Normal when normals != null:
WriteNormal(type, normals[vertexIndex], target, normalWs != null ? normalWs[vertexIndex] : null);
break;
case MdlFile.VertexUsage.Tangent1 when tangents != null:
WriteTangent(type, tangents[vertexIndex], target);
break;
case MdlFile.VertexUsage.Tangent2 when tangents2 != null:
WriteTangent(type, tangents2[vertexIndex], target);
break;
case MdlFile.VertexUsage.Color when colors != null:
WriteColor(type, colors[vertexIndex], target);
break;
case MdlFile.VertexUsage.BlendIndices when boneWeights != null:
WriteBlendIndices(type, boneWeights[vertexIndex], target);
break;
case MdlFile.VertexUsage.BlendWeights when boneWeights != null:
WriteBlendWeights(type, boneWeights[vertexIndex], decimated.BlendWeightEncoding, target);
break;
case MdlFile.VertexUsage.UV when format.UvChannelCount > 0:
if (!uvLookup.TryGetValue(ElementKey.From(element), out var uvElement))
{
reason = "UV mapping missing.";
return false;
}
WriteUv(type, uvElement, uvChannels, vertexIndex, target);
break;
}
}
}
updatedMesh.VertexCount = (ushort)vertexCount;
var newSubMeshes = new List<MdlStructs.SubmeshStruct>(originalSubMeshes.Length);
var indexList = new List<int>();
for (var subMeshIndex = 0; subMeshIndex < originalSubMeshes.Length; subMeshIndex++)
{
var subMeshIndices = decimatedSubMeshIndices[subMeshIndex];
if (subMeshIndices.Any(index => index < 0 || index >= vertexCount))
{
reason = "Decimated indices out of range.";
return false;
}
var offset = indexList.Count;
indexList.AddRange(subMeshIndices);
var updatedSubMesh = originalSubMeshes[subMeshIndex] with
{
IndexOffset = (uint)offset,
IndexCount = (uint)subMeshIndices.Length,
};
newSubMeshes.Add(updatedSubMesh);
}
updatedSubMeshes = newSubMeshes.ToArray();
indices = indexList.ToArray();
vertexStreams = streamBuffers;
return true;
}
private static bool TryBuildSubMeshIndices(
MdlFile mdl,
int lodIndex,
MeshStruct mesh,
MdlStructs.SubmeshStruct[] meshSubMeshes,
out int[][] subMeshIndices,
out string? reason)
{
reason = null;
subMeshIndices = new int[meshSubMeshes.Length][];
var meshIndices = ReadIndices(mdl, lodIndex, mesh);
for (var subMeshIndex = 0; subMeshIndex < meshSubMeshes.Length; subMeshIndex++)
{
var subMesh = meshSubMeshes[subMeshIndex];
if (subMesh.IndexCount == 0)
{
subMeshIndices[subMeshIndex] = [];
continue;
}
var relativeOffset = (int)(subMesh.IndexOffset - mesh.StartIndex);
if (relativeOffset < 0 || relativeOffset + subMesh.IndexCount > meshIndices.Length)
{
reason = "Submesh index range out of bounds.";
return false;
}
var slice = meshIndices.Skip(relativeOffset).Take((int)subMesh.IndexCount).Select(static i => (int)i).ToArray();
subMeshIndices[subMeshIndex] = slice;
}
return true;
}
private static byte[] BuildIndexBytes(List<ushort> indices)
{
var indexBytes = new byte[indices.Count * sizeof(ushort)];
for (var i = 0; i < indices.Count; i++)
{
BinaryPrimitives.WriteUInt16LittleEndian(indexBytes.AsSpan(i * 2, 2), indices[i]);
}
return indexBytes;
}
private static int[] ReadIndices(MdlFile mdl, int lodIndex, MeshStruct mesh)
{
using var reader = new BinaryReader(new MemoryStream(mdl.RemainingData));
reader.BaseStream.Position = mdl.IndexOffset[lodIndex] + mesh.StartIndex * sizeof(ushort);
var values = reader.ReadStructuresAsArray<ushort>((int)mesh.IndexCount);
return values.Select(static i => (int)i).ToArray();
}
private static byte[][] CopyVertexStreams(MdlFile mdl, int lodIndex, MeshStruct mesh)
{
var streams = new byte[MaxStreams][];
for (var stream = 0; stream < MaxStreams; stream++)
{
var stride = mesh.VertexBufferStride(stream);
if (stride == 0 || mesh.VertexCount == 0)
{
streams[stream] = [];
continue;
}
var size = stride * mesh.VertexCount;
var offset = mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(stream);
streams[stream] = mdl.RemainingData.AsSpan((int)offset, size).ToArray();
}
return streams;
}
private static MdlStructs.SubmeshStruct[] CopySubMeshes(MdlStructs.SubmeshStruct[] source, int newMeshIndexBase, uint meshStartIndex)
{
var result = new MdlStructs.SubmeshStruct[source.Length];
for (var i = 0; i < source.Length; i++)
{
var relativeOffset = (int)(source[i].IndexOffset - meshStartIndex);
result[i] = source[i] with
{
IndexOffset = (uint)(newMeshIndexBase + relativeOffset),
};
}
return result;
}
private static MdlStructs.SubmeshStruct[] OffsetSubMeshes(MdlStructs.SubmeshStruct[] source, int meshIndexBase)
{
var result = new MdlStructs.SubmeshStruct[source.Length];
for (var i = 0; i < source.Length; i++)
{
result[i] = source[i] with
{
IndexOffset = (uint)(meshIndexBase + source[i].IndexOffset),
};
}
return result;
}
private static bool TryBuildVertexFormat(MdlStructs.VertexDeclarationStruct declaration, out VertexFormat format, out string? reason)
{
reason = null;
format = default!;
var elements = declaration.VertexElements;
foreach (var element in elements)
{
if (element.Stream >= MaxStreams)
{
reason = "Vertex stream index out of range.";
return false;
}
var usage = (MdlFile.VertexUsage)element.Usage;
var type = (MdlFile.VertexType)element.Type;
if (!SupportedUsages.Contains(usage))
{
reason = $"Unsupported usage {usage}.";
return false;
}
if (!SupportedTypes.Contains(type))
{
reason = $"Unsupported vertex type {type}.";
return false;
}
}
var positionElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Position).ToArray();
if (positionElements.Length != 1)
{
reason = "Expected single position element.";
return false;
}
var positionType = (MdlFile.VertexType)positionElements[0].Type;
if (positionType != MdlFile.VertexType.Single3 && positionType != MdlFile.VertexType.Single4)
{
reason = "Unsupported position element type.";
return false;
}
var normalElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Normal).ToArray();
if (normalElements.Length > 1)
{
reason = "Multiple normal elements unsupported.";
return false;
}
if (normalElements.Length == 1)
{
var normalType = (MdlFile.VertexType)normalElements[0].Type;
if (normalType != MdlFile.VertexType.Single3
&& normalType != MdlFile.VertexType.Single4
&& normalType != MdlFile.VertexType.NByte4
&& normalType != MdlFile.VertexType.NShort4)
{
reason = "Unsupported normal element type.";
return false;
}
}
var tangent1Elements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Tangent1).ToArray();
if (tangent1Elements.Length > 1)
{
reason = "Multiple tangent1 elements unsupported.";
return false;
}
if (tangent1Elements.Length == 1)
{
var tangentType = (MdlFile.VertexType)tangent1Elements[0].Type;
if (tangentType != MdlFile.VertexType.Single4
&& tangentType != MdlFile.VertexType.NByte4
&& tangentType != MdlFile.VertexType.NShort4)
{
reason = "Unsupported tangent1 element type.";
return false;
}
}
var tangent2Elements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Tangent2).ToArray();
if (tangent2Elements.Length > 1)
{
reason = "Multiple tangent2 elements unsupported.";
return false;
}
if (tangent2Elements.Length == 1)
{
var tangentType = (MdlFile.VertexType)tangent2Elements[0].Type;
if (tangentType != MdlFile.VertexType.Single4
&& tangentType != MdlFile.VertexType.NByte4
&& tangentType != MdlFile.VertexType.NShort4)
{
reason = "Unsupported tangent2 element type.";
return false;
}
}
var colorElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Color).ToArray();
if (colorElements.Length > 1)
{
reason = "Multiple color elements unsupported.";
return false;
}
MdlStructs.VertexElement? colorElement = null;
if (colorElements.Length == 1)
{
var colorType = (MdlFile.VertexType)colorElements[0].Type;
if (colorType != MdlFile.VertexType.UByte4
&& colorType != MdlFile.VertexType.NByte4
&& colorType != MdlFile.VertexType.Single4
&& colorType != MdlFile.VertexType.Short4
&& colorType != MdlFile.VertexType.NShort4
&& colorType != MdlFile.VertexType.UShort4)
{
reason = "Unsupported color element type.";
return false;
}
colorElement = colorElements[0];
}
var blendIndicesElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.BlendIndices).ToArray();
var blendWeightsElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.BlendWeights).ToArray();
if (blendIndicesElements.Length != blendWeightsElements.Length)
{
reason = "Blend indices/weights mismatch.";
return false;
}
if (blendIndicesElements.Length > 1 || blendWeightsElements.Length > 1)
{
reason = "Multiple blend elements unsupported.";
return false;
}
if (blendIndicesElements.Length == 1)
{
var indexType = (MdlFile.VertexType)blendIndicesElements[0].Type;
if (indexType != MdlFile.VertexType.UByte4 && indexType != MdlFile.VertexType.UShort4)
{
reason = "Unsupported blend index type.";
return false;
}
var weightType = (MdlFile.VertexType)blendWeightsElements[0].Type;
if (weightType != MdlFile.VertexType.UByte4
&& weightType != MdlFile.VertexType.NByte4
&& weightType != MdlFile.VertexType.Single4
&& weightType != MdlFile.VertexType.UShort4
&& weightType != MdlFile.VertexType.NShort4)
{
reason = "Unsupported blend weight type.";
return false;
}
}
if (!TryBuildUvElements(elements, out var uvElements, out var uvChannelCount, out reason))
{
return false;
}
var positionElement = positionElements[0];
var sortedElements = elements.OrderBy(static element => element.Offset).ToList();
format = new VertexFormat(
sortedElements,
positionElement,
normalElements.Length == 1 ? normalElements[0] : (MdlStructs.VertexElement?)null,
tangent1Elements.Length == 1 ? tangent1Elements[0] : (MdlStructs.VertexElement?)null,
tangent2Elements.Length == 1 ? tangent2Elements[0] : (MdlStructs.VertexElement?)null,
colorElement,
blendIndicesElements.Length == 1 ? blendIndicesElements[0] : (MdlStructs.VertexElement?)null,
blendWeightsElements.Length == 1 ? blendWeightsElements[0] : (MdlStructs.VertexElement?)null,
uvElements,
uvChannelCount);
return true;
}
private static bool TryBuildUvElements(
IReadOnlyList<MdlStructs.VertexElement> elements,
out List<UvElementPacking> uvElements,
out int uvChannelCount,
out string? reason)
{
uvElements = [];
uvChannelCount = 0;
reason = null;
var uvList = elements
.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.UV)
.OrderBy(static e => e.UsageIndex)
.ToList();
foreach (var element in uvList)
{
var type = (MdlFile.VertexType)element.Type;
if (type == MdlFile.VertexType.Half2
|| type == MdlFile.VertexType.Single2
|| type == MdlFile.VertexType.Short2
|| type == MdlFile.VertexType.NShort2
|| type == MdlFile.VertexType.UShort2
|| type == MdlFile.VertexType.Single1)
{
if (uvChannelCount + 1 > MaxUvChannels)
{
reason = "Too many UV channels.";
return false;
}
uvElements.Add(new UvElementPacking(element, uvChannelCount, null));
uvChannelCount += 1;
}
else if (type == MdlFile.VertexType.Half4
|| type == MdlFile.VertexType.Single4
|| type == MdlFile.VertexType.Short4
|| type == MdlFile.VertexType.NShort4
|| type == MdlFile.VertexType.UShort4)
{
if (uvChannelCount + 2 > MaxUvChannels)
{
reason = "Too many UV channels.";
return false;
}
uvElements.Add(new UvElementPacking(element, uvChannelCount, uvChannelCount + 1));
uvChannelCount += 2;
}
else
{
reason = "Unsupported UV type.";
return false;
}
}
return true;
}
private static bool HasShapeData(MdlFile mdl)
=> mdl.Shapes.Length > 0
|| mdl.ShapeMeshes.Length > 0
|| mdl.ShapeValues.Length > 0
|| mdl.NeckMorphs.Length > 0;
private static Vector3d ReadPosition(MdlFile.VertexType type, BinaryReader reader)
{
switch (type)
{
case MdlFile.VertexType.Single3:
return new Vector3d(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
case MdlFile.VertexType.Single4:
var x = reader.ReadSingle();
var y = reader.ReadSingle();
var z = reader.ReadSingle();
_ = reader.ReadSingle();
return new Vector3d(x, y, z);
default:
throw new InvalidOperationException($"Unsupported position type {type}");
}
}
private static Vector3d ReadPositionWithW(BinaryReader reader, out float w)
{
var x = reader.ReadSingle();
var y = reader.ReadSingle();
var z = reader.ReadSingle();
w = reader.ReadSingle();
return new Vector3d(x, y, z);
}
private static Vector3 ReadNormal(MdlFile.VertexType type, BinaryReader reader)
{
switch (type)
{
case MdlFile.VertexType.Single3:
return new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
case MdlFile.VertexType.Single4:
var x = reader.ReadSingle();
var y = reader.ReadSingle();
var z = reader.ReadSingle();
_ = reader.ReadSingle();
return new Vector3(x, y, z);
case MdlFile.VertexType.NByte4:
return ReadNByte4(reader).ToVector3();
case MdlFile.VertexType.NShort4:
return ReadNShort4(reader).ToVector3();
default:
throw new InvalidOperationException($"Unsupported normal type {type}");
}
}
private static Vector3 ReadNormalWithW(BinaryReader reader, out float w)
{
var x = reader.ReadSingle();
var y = reader.ReadSingle();
var z = reader.ReadSingle();
w = reader.ReadSingle();
return new Vector3(x, y, z);
}
private static Vector4 ReadTangent(MdlFile.VertexType type, BinaryReader reader)
{
return type switch
{
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.NByte4 => ReadNByte4(reader),
MdlFile.VertexType.NShort4 => ReadNShort4(reader),
_ => throw new InvalidOperationException($"Unsupported tangent type {type}"),
};
}
private static Vector4 ReadColor(MdlFile.VertexType type, BinaryReader reader)
{
return type switch
{
MdlFile.VertexType.UByte4 => ReadUByte4(reader),
MdlFile.VertexType.NByte4 => ReadUByte4(reader),
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Short4 => ReadShort4(reader),
MdlFile.VertexType.NShort4 => ReadUShort4Normalized(reader),
MdlFile.VertexType.UShort4 => ReadUShort4Normalized(reader),
_ => throw new InvalidOperationException($"Unsupported color type {type}"),
};
}
private static void NormalizeTangents(Vector4[]? tangents, bool clampW)
{
if (tangents == null)
{
return;
}
for (var i = 0; i < tangents.Length; i++)
{
var tangent = tangents[i];
var x = tangent.x;
var y = tangent.y;
var z = tangent.z;
var w = tangent.w;
var length = MathF.Sqrt((x * x) + (y * y) + (z * z));
if (length > 1e-6f)
{
x /= length;
y /= length;
z /= length;
}
if (clampW)
{
w = w >= 0f ? 1f : -1f;
}
tangents[i] = new Vector4(x, y, z, w);
}
}
private static void ReadUv(MdlFile.VertexType type, BinaryReader reader, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex)
{
if (type == MdlFile.VertexType.Half2
|| type == MdlFile.VertexType.Single2
|| type == MdlFile.VertexType.Short2
|| type == MdlFile.VertexType.NShort2
|| type == MdlFile.VertexType.UShort2
|| type == MdlFile.VertexType.Single1)
{
var uv = type switch
{
MdlFile.VertexType.Half2 => new Vector2(ReadHalf(reader), ReadHalf(reader)),
MdlFile.VertexType.Single2 => new Vector2(reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Short2 => ReadShort2(reader),
MdlFile.VertexType.NShort2 => ReadUShort2Normalized(reader),
MdlFile.VertexType.UShort2 => ReadUShort2Normalized(reader),
MdlFile.VertexType.Single1 => new Vector2(reader.ReadSingle(), 0f),
_ => Vector2.Zero,
};
uvChannels[mapping.FirstChannel][vertexIndex] = uv;
return;
}
if (type == MdlFile.VertexType.Half4
|| type == MdlFile.VertexType.Single4
|| type == MdlFile.VertexType.Short4
|| type == MdlFile.VertexType.NShort4
|| type == MdlFile.VertexType.UShort4)
{
var uv = type switch
{
MdlFile.VertexType.Half4 => new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader)),
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Short4 => ReadShort4(reader),
MdlFile.VertexType.NShort4 => ReadUShort4Normalized(reader),
MdlFile.VertexType.UShort4 => ReadUShort4Normalized(reader),
_ => new Vector4(0f, 0f, 0f, 0f),
};
uvChannels[mapping.FirstChannel][vertexIndex] = new Vector2(uv.x, uv.y);
if (mapping.SecondChannel.HasValue)
{
uvChannels[mapping.SecondChannel.Value][vertexIndex] = new Vector2(uv.z, uv.w);
}
}
}
private static int[] ReadIndices(MdlFile.VertexType type, BinaryReader reader)
{
return type switch
{
MdlFile.VertexType.UByte4 => new[] { (int)reader.ReadByte(), (int)reader.ReadByte(), (int)reader.ReadByte(), (int)reader.ReadByte() },
MdlFile.VertexType.UShort4 => new[] { (int)reader.ReadUInt16(), (int)reader.ReadUInt16(), (int)reader.ReadUInt16(), (int)reader.ReadUInt16() },
_ => throw new InvalidOperationException($"Unsupported indices type {type}"),
};
}
private static float[] ReadWeights(MdlFile.VertexType type, BinaryReader reader, BlendWeightEncoding encoding)
{
return type switch
{
MdlFile.VertexType.UByte4 => ReadUByte4(reader).ToFloatArray(),
MdlFile.VertexType.NByte4 => ReadUByte4(reader).ToFloatArray(),
MdlFile.VertexType.Single4 => new[] { reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle() },
MdlFile.VertexType.NShort4 => ReadUShort4Normalized(reader).ToFloatArray(),
MdlFile.VertexType.UShort4 => encoding == BlendWeightEncoding.UShortAsByte
? ReadUShort4AsByte(reader)
: ReadUShort4Normalized(reader).ToFloatArray(),
_ => throw new InvalidOperationException($"Unsupported weights type {type}"),
};
}
private static float[] ReadUShort4AsByte(BinaryReader reader)
{
var w0 = reader.ReadUInt16();
var w1 = reader.ReadUInt16();
var w2 = reader.ReadUInt16();
var w3 = reader.ReadUInt16();
return new[] { w0 / 255f, w1 / 255f, w2 / 255f, w3 / 255f };
}
private static Vector4 ReadUByte4(BinaryReader reader)
{
return new Vector4(
reader.ReadByte() / 255f,
reader.ReadByte() / 255f,
reader.ReadByte() / 255f,
reader.ReadByte() / 255f);
}
private static Vector4 ReadNByte4(BinaryReader reader)
{
var value = ReadUByte4(reader);
return (value * 2f) - new Vector4(1f, 1f, 1f, 1f);
}
private static Vector2 ReadShort2(BinaryReader reader)
=> new(reader.ReadInt16(), reader.ReadInt16());
private static Vector4 ReadShort4(BinaryReader reader)
=> new(reader.ReadInt16(), reader.ReadInt16(), reader.ReadInt16(), reader.ReadInt16());
/* these really don't have a use currently, we don't need to read raw unnormalized ushorts :3
private static Vector2 ReadUShort2(BinaryReader reader)
=> new(reader.ReadUInt16(), reader.ReadUInt16());
private static Vector4 ReadUShort4(BinaryReader reader)
=> new(reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16(), reader.ReadUInt16());
*/
private static Vector2 ReadUShort2Normalized(BinaryReader reader)
=> new(reader.ReadUInt16() / (float)ushort.MaxValue, reader.ReadUInt16() / (float)ushort.MaxValue);
private static Vector4 ReadUShort4Normalized(BinaryReader reader)
=> new(reader.ReadUInt16() / (float)ushort.MaxValue, reader.ReadUInt16() / (float)ushort.MaxValue, reader.ReadUInt16() / (float)ushort.MaxValue, reader.ReadUInt16() / (float)ushort.MaxValue);
private static Vector4 ReadNShort4(BinaryReader reader)
{
var value = ReadUShort4Normalized(reader);
return (value * 2f) - new Vector4(1f, 1f, 1f, 1f);
}
private static Vector4 ReadAndDiscard(MdlFile.VertexType type, BinaryReader reader)
{
switch (type)
{
case MdlFile.VertexType.Single1:
return new Vector4(reader.ReadSingle(), 0, 0, 0);
case MdlFile.VertexType.Single2:
return new Vector4(reader.ReadSingle(), reader.ReadSingle(), 0, 0);
case MdlFile.VertexType.Single3:
return new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0);
case MdlFile.VertexType.Single4:
return new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
case MdlFile.VertexType.Half2:
return new Vector4(ReadHalf(reader), ReadHalf(reader), 0, 0);
case MdlFile.VertexType.Half4:
return new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader));
case MdlFile.VertexType.UByte4:
return ReadUByte4(reader);
case MdlFile.VertexType.NByte4:
return ReadUByte4(reader);
case MdlFile.VertexType.Short2:
{
var value = ReadShort2(reader);
return new Vector4(value.x, value.y, 0, 0);
}
case MdlFile.VertexType.Short4:
return ReadShort4(reader);
case MdlFile.VertexType.NShort2:
{
var value = ReadUShort2Normalized(reader);
return new Vector4(value.x, value.y, 0, 0);
}
case MdlFile.VertexType.NShort4:
return ReadUShort4Normalized(reader);
case MdlFile.VertexType.UShort2:
{
var value = ReadUShort2Normalized(reader);
return new Vector4(value.x, value.y, 0, 0);
}
case MdlFile.VertexType.UShort4:
return ReadUShort4Normalized(reader);
default:
return new Vector4(0f, 0f, 0f, 0f);
}
}
private static void WritePosition(MdlFile.VertexType type, Vector3d value, Span<byte> target, float? wOverride = null)
{
if (type == MdlFile.VertexType.Single4 && wOverride.HasValue)
{
WriteVector4(type, new Vector4((float)value.x, (float)value.y, (float)value.z, wOverride.Value), target);
return;
}
WriteVector3(type, new Vector3((float)value.x, (float)value.y, (float)value.z), target);
}
private static void WriteNormal(MdlFile.VertexType type, Vector3 value, Span<byte> target, float? wOverride = null)
{
if (type == MdlFile.VertexType.Single4 && wOverride.HasValue)
{
WriteVector4(type, new Vector4(value.x, value.y, value.z, wOverride.Value), target);
return;
}
WriteVector3(type, value, target, normalized: type == MdlFile.VertexType.NByte4 || type == MdlFile.VertexType.NShort4);
}
private static void WriteTangent(MdlFile.VertexType type, Vector4 value, Span<byte> target)
{
if (type == MdlFile.VertexType.NByte4)
{
WriteNByte4(value, target);
return;
}
if (type == MdlFile.VertexType.NShort4)
{
WriteNShort4(value, target);
return;
}
WriteVector4(type, value, target);
}
private static void WriteColor(MdlFile.VertexType type, Vector4 value, Span<byte> target)
{
if (type == MdlFile.VertexType.Single4
|| type == MdlFile.VertexType.Short4
|| type == MdlFile.VertexType.NShort4
|| type == MdlFile.VertexType.UShort4)
{
WriteVector4(type, value, target);
return;
}
WriteUByte4(value, target);
}
private static void WriteBlendIndices(MdlFile.VertexType type, BoneWeight weights, Span<byte> target)
{
if (type == MdlFile.VertexType.UByte4)
{
target[0] = (byte)Math.Clamp(weights.index0, 0, 255);
target[1] = (byte)Math.Clamp(weights.index1, 0, 255);
target[2] = (byte)Math.Clamp(weights.index2, 0, 255);
target[3] = (byte)Math.Clamp(weights.index3, 0, 255);
return;
}
if (type == MdlFile.VertexType.UShort4)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShort(weights.index0));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShort(weights.index1));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShort(weights.index2));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShort(weights.index3));
}
}
private static void WriteBlendWeights(MdlFile.VertexType type, BoneWeight weights, BlendWeightEncoding encoding, Span<byte> target)
{
if (type == MdlFile.VertexType.Single4)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], weights.weight0);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), weights.weight1);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), weights.weight2);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), weights.weight3);
return;
}
if (type != MdlFile.VertexType.UByte4
&& type != MdlFile.VertexType.NByte4
&& type != MdlFile.VertexType.UShort4
&& type != MdlFile.VertexType.NShort4)
{
return;
}
var w0 = Clamp01(weights.weight0);
var w1 = Clamp01(weights.weight1);
var w2 = Clamp01(weights.weight2);
var w3 = Clamp01(weights.weight3);
if (type == MdlFile.VertexType.UShort4 && encoding == BlendWeightEncoding.UShortAsByte)
{
WriteUShort4AsByte(w0, w1, w2, w3, target);
return;
}
if (type == MdlFile.VertexType.UShort4 || type == MdlFile.VertexType.NShort4)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShortNormalized(w0));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShortNormalized(w1));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShortNormalized(w2));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShortNormalized(w3));
return;
}
WriteByteWeights(w0, w1, w2, w3, target);
}
private static void WriteUShort4AsByte(float w0, float w1, float w2, float w3, Span<byte> target)
{
QuantizeByteWeights(w0, w1, w2, w3, out var b0, out var b1, out var b2, out var b3);
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], (ushort)b0);
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), (ushort)b1);
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), (ushort)b2);
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), (ushort)b3);
}
private static void WriteUv(MdlFile.VertexType type, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex, Span<byte> target)
{
if (type == MdlFile.VertexType.Half2
|| type == MdlFile.VertexType.Single2
|| type == MdlFile.VertexType.Short2
|| type == MdlFile.VertexType.NShort2
|| type == MdlFile.VertexType.UShort2
|| type == MdlFile.VertexType.Single1)
{
var uv = uvChannels[mapping.FirstChannel][vertexIndex];
WriteVector2(type, uv, target);
return;
}
if (type == MdlFile.VertexType.Half4
|| type == MdlFile.VertexType.Single4
|| type == MdlFile.VertexType.Short4
|| type == MdlFile.VertexType.NShort4
|| type == MdlFile.VertexType.UShort4)
{
var uv0 = uvChannels[mapping.FirstChannel][vertexIndex];
var uv1 = mapping.SecondChannel.HasValue
? uvChannels[mapping.SecondChannel.Value][vertexIndex]
: Vector2.Zero;
WriteVector4(type, new Vector4(uv0.x, uv0.y, uv1.x, uv1.y), target);
}
}
private static void WriteVector2(MdlFile.VertexType type, Vector2 value, Span<byte> target)
{
if (type == MdlFile.VertexType.Single1)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
return;
}
if (type == MdlFile.VertexType.Single2)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
return;
}
if (type == MdlFile.VertexType.Half2)
{
WriteHalf(target[..2], value.x);
WriteHalf(target.Slice(2, 2), value.y);
return;
}
if (type == MdlFile.VertexType.Short2)
{
WriteShort2(value, target);
return;
}
if (type == MdlFile.VertexType.NShort2)
{
WriteUShort2Normalized(value, target);
return;
}
if (type == MdlFile.VertexType.UShort2)
{
WriteUShort2Normalized(value, target);
}
}
private static void WriteVector3(MdlFile.VertexType type, Vector3 value, Span<byte> target, bool normalized = false)
{
if (type == MdlFile.VertexType.Single3)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z);
return;
}
if (type == MdlFile.VertexType.Single4)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), 1f);
return;
}
if (type == MdlFile.VertexType.NByte4 && normalized)
{
WriteNByte4(new Vector4(value.x, value.y, value.z, 0f), target);
return;
}
if (type == MdlFile.VertexType.NShort4 && normalized)
{
WriteNShort4(new Vector4(value.x, value.y, value.z, 0f), target);
}
}
private static void WriteVector4(MdlFile.VertexType type, Vector4 value, Span<byte> target)
{
if (type == MdlFile.VertexType.Single4)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], value.x);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), value.y);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), value.z);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), value.w);
return;
}
if (type == MdlFile.VertexType.Half4)
{
WriteHalf(target[..2], value.x);
WriteHalf(target.Slice(2, 2), value.y);
WriteHalf(target.Slice(4, 2), value.z);
WriteHalf(target.Slice(6, 2), value.w);
return;
}
if (type == MdlFile.VertexType.Short4)
{
WriteShort4(value, target);
return;
}
if (type == MdlFile.VertexType.NShort4)
{
WriteUShort4Normalized(value, target);
return;
}
if (type == MdlFile.VertexType.UShort4)
{
WriteUShort4Normalized(value, target);
}
}
private static void WriteUByte4(Vector4 value, Span<byte> target)
{
target[0] = ToByte(Clamp01(value.x));
target[1] = ToByte(Clamp01(value.y));
target[2] = ToByte(Clamp01(value.z));
target[3] = ToByte(Clamp01(value.w));
}
private static void WriteNByte4(Vector4 value, Span<byte> target)
{
var normalized = (value * 0.5f) + new Vector4(0.5f, 0.5f, 0.5f, 0.5f);
WriteUByte4(normalized, target);
}
private static void WriteShort2(Vector2 value, Span<byte> target)
{
BinaryPrimitives.WriteInt16LittleEndian(target[..2], ToShort(value.x));
BinaryPrimitives.WriteInt16LittleEndian(target.Slice(2, 2), ToShort(value.y));
}
private static void WriteShort4(Vector4 value, Span<byte> target)
{
BinaryPrimitives.WriteInt16LittleEndian(target[..2], ToShort(value.x));
BinaryPrimitives.WriteInt16LittleEndian(target.Slice(2, 2), ToShort(value.y));
BinaryPrimitives.WriteInt16LittleEndian(target.Slice(4, 2), ToShort(value.z));
BinaryPrimitives.WriteInt16LittleEndian(target.Slice(6, 2), ToShort(value.w));
}
/* same thing as read here, we don't need to write currently either
private static void WriteUShort2(Vector2 value, Span<byte> target)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShort(value.x));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShort(value.y));
}
private static void WriteUShort4(Vector4 value, Span<byte> target)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShort(value.x));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShort(value.y));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShort(value.z));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShort(value.w));
}
*/
private static void WriteUShort2Normalized(Vector2 value, Span<byte> target)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShortNormalized(value.x));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShortNormalized(value.y));
}
private static void WriteUShort4Normalized(Vector4 value, Span<byte> target)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShortNormalized(value.x));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShortNormalized(value.y));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShortNormalized(value.z));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShortNormalized(value.w));
}
private static void WriteNShort4(Vector4 value, Span<byte> target)
{
BinaryPrimitives.WriteUInt16LittleEndian(target[..2], ToUShortSnorm(value.x));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(2, 2), ToUShortSnorm(value.y));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(4, 2), ToUShortSnorm(value.z));
BinaryPrimitives.WriteUInt16LittleEndian(target.Slice(6, 2), ToUShortSnorm(value.w));
}
private static void WriteHalf(Span<byte> target, float value)
{
var half = (Half)value;
BinaryPrimitives.WriteUInt16LittleEndian(target, BitConverter.HalfToUInt16Bits(half));
}
private static float ReadHalf(BinaryReader reader)
=> (float)BitConverter.UInt16BitsToHalf(reader.ReadUInt16());
private static float Clamp01(float value)
=> Math.Clamp(value, 0f, 1f);
private static float ClampMinusOneToOne(float value)
=> Math.Clamp(value, -1f, 1f);
private static byte ToByte(float value)
=> (byte)Math.Clamp((int)Math.Round(value * 255f), 0, 255);
private static void WriteByteWeights(float w0, float w1, float w2, float w3, Span<byte> target)
{
QuantizeByteWeights(w0, w1, w2, w3, out var b0, out var b1, out var b2, out var b3);
target[0] = (byte)b0;
target[1] = (byte)b1;
target[2] = (byte)b2;
target[3] = (byte)b3;
}
private static void QuantizeByteWeights(float w0, float w1, float w2, float w3, out int b0, out int b1, out int b2, out int b3)
{
var sum = w0 + w1 + w2 + w3;
if (sum <= 1e-6f)
{
w0 = 1f;
w1 = 0f;
w2 = 0f;
w3 = 0f;
sum = 1f;
}
var targetSum = (int)MathF.Round(sum * 255f);
if (sum > 0f && targetSum == 0)
{
targetSum = 1;
}
targetSum = Math.Clamp(targetSum, 0, 255);
if (targetSum == 0)
{
b0 = 0;
b1 = 0;
b2 = 0;
b3 = 0;
return;
}
var scale = targetSum / sum;
var scaled0 = w0 * scale;
var scaled1 = w1 * scale;
var scaled2 = w2 * scale;
var scaled3 = w3 * scale;
b0 = (int)MathF.Floor(scaled0);
b1 = (int)MathF.Floor(scaled1);
b2 = (int)MathF.Floor(scaled2);
b3 = (int)MathF.Floor(scaled3);
var remainder = targetSum - (b0 + b1 + b2 + b3);
if (remainder > 0)
{
Span<float> fractions = stackalloc float[4];
fractions[0] = scaled0 - b0;
fractions[1] = scaled1 - b1;
fractions[2] = scaled2 - b2;
fractions[3] = scaled3 - b3;
Span<int> order = stackalloc int[4] { 0, 1, 2, 3 };
for (var i = 0; i < order.Length - 1; i++)
{
for (var j = i + 1; j < order.Length; j++)
{
if (fractions[order[j]] > fractions[order[i]])
{
(order[i], order[j]) = (order[j], order[i]);
}
}
}
for (var i = 0; i < remainder && i < order.Length; i++)
{
switch (order[i])
{
case 0:
b0++;
break;
case 1:
b1++;
break;
case 2:
b2++;
break;
case 3:
b3++;
break;
}
}
}
b0 = Math.Clamp(b0, 0, 255);
b1 = Math.Clamp(b1, 0, 255);
b2 = Math.Clamp(b2, 0, 255);
b3 = Math.Clamp(b3, 0, 255);
}
private static short ToShort(float value)
=> (short)Math.Clamp((int)Math.Round(value), short.MinValue, short.MaxValue);
private static ushort ToUShort(int value)
=> (ushort)Math.Clamp(value, ushort.MinValue, ushort.MaxValue);
/*
private static ushort ToUShort(float value)
=> (ushort)Math.Clamp((int)Math.Round(value), ushort.MinValue, ushort.MaxValue);
*/
private static ushort ToUShortNormalized(float value)
=> (ushort)Math.Clamp((int)Math.Round(Clamp01(value) * ushort.MaxValue), ushort.MinValue, ushort.MaxValue);
private static ushort ToUShortSnorm(float value)
{
var normalized = (ClampMinusOneToOne(value) * 0.5f) + 0.5f;
return ToUShortNormalized(normalized);
}
private static void NormalizeWeights(float[] weights)
{
var sum = weights.Sum();
if (sum <= float.Epsilon)
{
return;
}
for (var i = 0; i < weights.Length; i++)
{
weights[i] /= sum;
}
}
private static void NormalizeWeights(ref float w0, ref float w1, ref float w2, ref float w3)
{
var sum = w0 + w1 + w2 + w3;
if (sum <= float.Epsilon)
{
return;
}
w0 /= sum;
w1 /= sum;
w2 /= sum;
w3 /= sum;
}
private static BlendWeightEncoding DetectBlendWeightEncoding(MdlFile mdl, int lodIndex, MeshStruct mesh, VertexFormat format)
{
if (!format.BlendWeightsElement.HasValue)
{
return BlendWeightEncoding.Default;
}
var blendWeightsElement = format.BlendWeightsElement.Value;
if ((MdlFile.VertexType)blendWeightsElement.Type != MdlFile.VertexType.UShort4)
{
return BlendWeightEncoding.Default;
}
var stride = mesh.VertexBufferStride(blendWeightsElement.Stream);
if (stride == 0 || mesh.VertexCount == 0)
{
return BlendWeightEncoding.Default;
}
var elementSize = GetElementSize(MdlFile.VertexType.UShort4);
var baseOffset = (int)(mdl.VertexOffset[lodIndex] + mesh.VertexBufferOffset(blendWeightsElement.Stream));
var data = mdl.RemainingData.AsSpan();
for (var vertexIndex = 0; vertexIndex < mesh.VertexCount; vertexIndex++)
{
var offset = baseOffset + (vertexIndex * stride) + blendWeightsElement.Offset;
if (offset < 0 || offset + elementSize > data.Length)
{
return BlendWeightEncoding.Default;
}
var w0 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset, 2));
var w1 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset + 2, 2));
var w2 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset + 4, 2));
var w3 = BinaryPrimitives.ReadUInt16LittleEndian(data.Slice(offset + 6, 2));
if (w0 > byte.MaxValue || w1 > byte.MaxValue || w2 > byte.MaxValue || w3 > byte.MaxValue)
{
return BlendWeightEncoding.Default;
}
}
return BlendWeightEncoding.UShortAsByte;
}
private static int GetElementSize(MdlFile.VertexType type)
=> type switch
{
MdlFile.VertexType.Single1 => 4,
MdlFile.VertexType.Single2 => 8,
MdlFile.VertexType.Single3 => 12,
MdlFile.VertexType.Single4 => 16,
MdlFile.VertexType.Half2 => 4,
MdlFile.VertexType.Half4 => 8,
MdlFile.VertexType.UByte4 => 4,
MdlFile.VertexType.NByte4 => 4,
MdlFile.VertexType.Short2 => 4,
MdlFile.VertexType.Short4 => 8,
MdlFile.VertexType.NShort2 => 4,
MdlFile.VertexType.NShort4 => 8,
MdlFile.VertexType.UShort2 => 4,
MdlFile.VertexType.UShort4 => 8,
_ => throw new InvalidOperationException($"Unsupported vertex type {type}"),
};
private enum BlendWeightEncoding
{
Default,
UShortAsByte,
}
private readonly record struct ElementKey(byte Stream, byte Offset, byte Type, byte Usage, byte UsageIndex)
{
public static ElementKey From(MdlStructs.VertexElement element)
=> new(element.Stream, element.Offset, element.Type, element.Usage, element.UsageIndex);
}
private sealed class VertexFormat
{
public VertexFormat(
List<MdlStructs.VertexElement> sortedElements,
MdlStructs.VertexElement positionElement,
MdlStructs.VertexElement? normalElement,
MdlStructs.VertexElement? tangent1Element,
MdlStructs.VertexElement? tangent2Element,
MdlStructs.VertexElement? colorElement,
MdlStructs.VertexElement? blendIndicesElement,
MdlStructs.VertexElement? blendWeightsElement,
List<UvElementPacking> uvElements,
int uvChannelCount)
{
SortedElements = sortedElements;
PositionElement = positionElement;
NormalElement = normalElement;
Tangent1Element = tangent1Element;
Tangent2Element = tangent2Element;
ColorElement = colorElement;
BlendIndicesElement = blendIndicesElement;
BlendWeightsElement = blendWeightsElement;
UvElements = uvElements;
UvChannelCount = uvChannelCount;
}
public List<MdlStructs.VertexElement> SortedElements { get; }
public MdlStructs.VertexElement PositionElement { get; }
public MdlStructs.VertexElement? NormalElement { get; }
public MdlStructs.VertexElement? Tangent1Element { get; }
public MdlStructs.VertexElement? Tangent2Element { get; }
public MdlStructs.VertexElement? ColorElement { get; }
public MdlStructs.VertexElement? BlendIndicesElement { get; }
public MdlStructs.VertexElement? BlendWeightsElement { get; }
public List<UvElementPacking> UvElements { get; }
public int UvChannelCount { get; }
public bool HasNormals => NormalElement.HasValue;
public bool HasTangent1 => Tangent1Element.HasValue;
public bool HasTangent2 => Tangent2Element.HasValue;
public bool HasColors => ColorElement.HasValue;
public bool HasSkinning => BlendIndicesElement.HasValue && BlendWeightsElement.HasValue;
public bool HasPositionW => (MdlFile.VertexType)PositionElement.Type == MdlFile.VertexType.Single4;
public bool HasNormalW => NormalElement.HasValue && (MdlFile.VertexType)NormalElement.Value.Type == MdlFile.VertexType.Single4;
}
private readonly record struct UvElementPacking(MdlStructs.VertexElement Element, int FirstChannel, int? SecondChannel);
private sealed class DecodedMeshData
{
public DecodedMeshData(
Vector3d[] positions,
Vector3[]? normals,
Vector4[]? tangents,
Vector4[]? tangents2,
Vector4[]? colors,
BoneWeight[]? boneWeights,
Vector2[][]? uvChannels,
float[]? positionWs,
float[]? normalWs,
BlendWeightEncoding blendWeightEncoding)
{
Positions = positions;
Normals = normals;
Tangents = tangents;
Tangents2 = tangents2;
Colors = colors;
BoneWeights = boneWeights;
UvChannels = uvChannels;
PositionWs = positionWs;
NormalWs = normalWs;
BlendWeightEncoding = blendWeightEncoding;
}
public Vector3d[] Positions { get; }
public Vector3[]? Normals { get; }
public Vector4[]? Tangents { get; }
public Vector4[]? Tangents2 { get; }
public Vector4[]? Colors { get; }
public BoneWeight[]? BoneWeights { get; }
public Vector2[][]? UvChannels { get; }
public float[]? PositionWs { get; }
public float[]? NormalWs { get; }
public BlendWeightEncoding BlendWeightEncoding { get; }
}
}
internal static class NanomeshVectorExtensions
{
public static Vector3 ToVector3(this Vector4 value)
=> new(value.x, value.y, value.z);
public static float[] ToFloatArray(this Vector4 value)
=> [value.x, value.y, value.z, value.w];
}