Files
LightlessClient/LightlessSync/Services/ModelDecimation/MdlDecimator.cs
cake 30717ba200 Merged Cake and Abel branched into 2.0.3 (#131)
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: defnotken <itsdefnotken@gmail.com>
Reviewed-on: #131
2026-01-05 00:45:14 +00:00

1463 lines
53 KiB
C#

using Lumina.Data.Parsing;
using Lumina.Extensions;
using MeshDecimator;
using MeshDecimator.Algorithms;
using MeshDecimator.Math;
using Microsoft.Extensions.Logging;
using Penumbra.GameData.Files.ModelStructs;
using System.Buffers.Binary;
using MdlFile = Penumbra.GameData.Files.MdlFile;
using MsLogger = Microsoft.Extensions.Logging.ILogger;
namespace LightlessSync.Services.ModelDecimation;
internal static class MdlDecimator
{
private const int MaxStreams = 3;
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.UV,
MdlFile.VertexUsage.Color,
MdlFile.VertexUsage.BlendWeights,
MdlFile.VertexUsage.BlendIndices,
];
private static readonly HashSet<MdlFile.VertexType> SupportedTypes =
[
MdlFile.VertexType.Single2,
MdlFile.VertexType.Single3,
MdlFile.VertexType.Single4,
MdlFile.VertexType.Half2,
MdlFile.VertexType.Half4,
MdlFile.VertexType.UByte4,
MdlFile.VertexType.NByte4,
];
public static bool TryDecimate(string sourcePath, string destinationPath, int triangleThreshold, double targetRatio, MsLogger logger)
{
try
{
if (!TryReadModelBytes(sourcePath, logger, out var data))
{
logger.LogInformation("Skipping model decimation; source file locked or unreadable: {Path}", sourcePath);
return false;
}
var mdl = new MdlFile(data);
if (!mdl.Valid)
{
logger.LogInformation("Skipping model decimation; invalid mdl: {Path}", sourcePath);
return false;
}
if (mdl.LodCount != 1)
{
logger.LogInformation("Skipping model decimation; unsupported LOD count for {Path}", sourcePath);
return false;
}
if (HasShapeData(mdl))
{
logger.LogInformation("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.LogInformation("Skipping model decimation; no meshes for {Path}", sourcePath);
return false;
}
if (lod.MeshCount == 0)
{
logger.LogInformation("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.LogInformation("Skipping model decimation; invalid LOD mesh range for {Path}", sourcePath);
return false;
}
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;
DecimationAlgorithm? decimationAlgorithm = null;
int? decimationUvChannelCount = null;
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 (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd
&& TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, triangleThreshold, targetRatio,
out updatedMesh,
out updatedSubMeshes,
out vertexStreams,
out indices,
out decimated,
ref decimationAlgorithm,
ref decimationUvChannelCount,
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.LogInformation("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,
int triangleThreshold,
double targetRatio,
out MeshStruct updatedMesh,
out MdlStructs.SubmeshStruct[] updatedSubMeshes,
out byte[][] vertexStreams,
out int[] indices,
out bool decimated,
ref DecimationAlgorithm? decimationAlgorithm,
ref int? decimationUvChannelCount,
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 < 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 * targetRatio);
if (targetTriangles < 1 || targetTriangles >= triangleCount)
{
return false;
}
var meshDecimatorMesh = BuildMesh(decoded, subMeshIndices);
var algorithm = GetOrCreateAlgorithm(format, ref decimationAlgorithm, ref decimationUvChannelCount, logger);
algorithm.Initialize(meshDecimatorMesh);
algorithm.DecimateMesh(targetTriangles);
var decimatedMesh = algorithm.ToMesh();
if (decimatedMesh.SubMeshCount != meshSubMeshes.Length)
{
logger.LogDebug("Mesh {MeshIndex} submesh count changed after decimation", meshIndex);
return false;
}
if (!TryEncodeMeshData(decimatedMesh, format, mesh, meshSubMeshes, 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 DecimationAlgorithm GetOrCreateAlgorithm(
VertexFormat format,
ref DecimationAlgorithm? decimationAlgorithm,
ref int? decimationUvChannelCount,
MsLogger logger)
{
var uvChannelCount = format.UvChannelCount;
if (decimationAlgorithm == null || decimationUvChannelCount != uvChannelCount)
{
decimationAlgorithm = MeshDecimation.CreateAlgorithm(Algorithm.Default);
decimationAlgorithm.Logger = logger;
decimationUvChannelCount = uvChannelCount;
}
return decimationAlgorithm;
}
private static Mesh BuildMesh(DecodedMeshData decoded, int[][] subMeshIndices)
{
var mesh = new Mesh(decoded.Positions, subMeshIndices);
if (decoded.Normals != null)
{
mesh.Normals = decoded.Normals;
}
if (decoded.Tangents != null)
{
mesh.Tangents = decoded.Tangents;
}
if (decoded.Colors != null)
{
mesh.Colors = decoded.Colors;
}
if (decoded.BoneWeights != null)
{
mesh.BoneWeights = decoded.BoneWeights;
}
if (decoded.UvChannels != null)
{
for (var channel = 0; channel < decoded.UvChannels.Length; channel++)
{
mesh.SetUVs(channel, decoded.UvChannels[channel]);
}
}
return mesh;
}
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.HasTangents ? new Vector4[vertexCount] : null;
Vector4[]? colors = format.HasColors ? new Vector4[vertexCount] : null;
BoneWeight[]? boneWeights = format.HasSkinning ? new BoneWeight[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 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++)
{
byte[]? 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:
positions[vertexIndex] = ReadPosition(type, stream);
break;
case MdlFile.VertexUsage.Normal when normals != null:
normals[vertexIndex] = ReadNormal(type, stream);
break;
case MdlFile.VertexUsage.Tangent1 when tangents != null:
tangents[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);
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.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;
}
NormalizeWeights(weights);
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, colors, boneWeights, uvChannels);
return true;
}
private static bool TryEncodeMeshData(
Mesh decimatedMesh,
VertexFormat format,
MeshStruct originalMesh,
MdlStructs.SubmeshStruct[] originalSubMeshes,
out MeshStruct updatedMesh,
out MdlStructs.SubmeshStruct[] updatedSubMeshes,
out byte[][] vertexStreams,
out int[] indices,
out string? reason)
{
updatedMesh = originalMesh;
updatedSubMeshes = [];
vertexStreams = [[], [], []];
indices = [];
reason = null;
var vertexCount = decimatedMesh.Vertices.Length;
if (vertexCount > ushort.MaxValue)
{
reason = "Vertex count exceeds ushort range.";
return false;
}
var normals = decimatedMesh.Normals;
var tangents = decimatedMesh.Tangents;
var colors = decimatedMesh.Colors;
var boneWeights = decimatedMesh.BoneWeights;
if (format.HasNormals && normals == null)
{
reason = "Missing normals after decimation.";
return false;
}
if (format.HasTangents && tangents == null)
{
reason = "Missing tangents after decimation.";
return false;
}
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;
}
var uvChannels = Array.Empty<Vector2[]>();
if (format.UvChannelCount > 0)
{
uvChannels = new Vector2[format.UvChannelCount][];
for (var channel = 0; channel < format.UvChannelCount; channel++)
{
if (decimatedMesh.GetUVDimension(channel) != 2)
{
reason = "Unsupported UV dimension after decimation.";
return false;
}
uvChannels[channel] = decimatedMesh.GetUVs2D(channel);
}
}
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, decimatedMesh.Vertices[vertexIndex], target);
break;
case MdlFile.VertexUsage.Normal when normals != null:
WriteNormal(type, normals[vertexIndex], target);
break;
case MdlFile.VertexUsage.Tangent1 when tangents != null:
WriteTangent(type, tangents[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], 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 = decimatedMesh.GetIndices(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)
{
reason = "Unsupported normal element type.";
return false;
}
}
var tangentElements = elements.Where(static e => (MdlFile.VertexUsage)e.Usage == MdlFile.VertexUsage.Tangent1).ToArray();
if (tangentElements.Length > 1)
{
reason = "Multiple tangent elements unsupported.";
return false;
}
if (tangentElements.Length == 1)
{
var tangentType = (MdlFile.VertexType)tangentElements[0].Type;
if (tangentType != MdlFile.VertexType.Single4 && tangentType != MdlFile.VertexType.NByte4)
{
reason = "Unsupported tangent 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)
{
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)
{
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)
{
reason = "Unsupported blend weight type.";
return false;
}
}
if (!TryBuildUvElements(elements, out var uvElements, out var uvChannelCount, out reason))
{
return false;
}
var sortedElements = elements.OrderBy(static element => element.Offset).ToList();
format = new VertexFormat(
sortedElements,
normalElements.Length == 1 ? normalElements[0] : (MdlStructs.VertexElement?)null,
tangentElements.Length == 1 ? tangentElements[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)
{
if (uvChannelCount + 1 > Mesh.UVChannelCount)
{
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)
{
if (uvChannelCount + 2 > Mesh.UVChannelCount)
{
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 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();
default:
throw new InvalidOperationException($"Unsupported normal type {type}");
}
}
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),
_ => 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()),
_ => throw new InvalidOperationException($"Unsupported color type {type}"),
};
}
private static void ReadUv(MdlFile.VertexType type, BinaryReader reader, UvElementPacking mapping, Vector2[][] uvChannels, int vertexIndex)
{
if (type == MdlFile.VertexType.Half2 || type == MdlFile.VertexType.Single2)
{
var uv = type == MdlFile.VertexType.Half2
? new Vector2(ReadHalf(reader), ReadHalf(reader))
: new Vector2(reader.ReadSingle(), reader.ReadSingle());
uvChannels[mapping.FirstChannel][vertexIndex] = uv;
return;
}
if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4)
{
var uv = type == MdlFile.VertexType.Half4
? new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader))
: new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle());
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 byte[] ReadIndices(MdlFile.VertexType type, BinaryReader reader)
{
return type switch
{
MdlFile.VertexType.UByte4 => new[] { reader.ReadByte(), reader.ReadByte(), reader.ReadByte(), reader.ReadByte() },
_ => throw new InvalidOperationException($"Unsupported indices type {type}"),
};
}
private static float[] ReadWeights(MdlFile.VertexType type, BinaryReader reader)
{
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() },
_ => throw new InvalidOperationException($"Unsupported weights type {type}"),
};
}
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 Vector4 ReadAndDiscard(MdlFile.VertexType type, BinaryReader reader)
{
return type switch
{
MdlFile.VertexType.Single2 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), 0, 0),
MdlFile.VertexType.Single3 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), 0),
MdlFile.VertexType.Single4 => new Vector4(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()),
MdlFile.VertexType.Half2 => new Vector4(ReadHalf(reader), ReadHalf(reader), 0, 0),
MdlFile.VertexType.Half4 => new Vector4(ReadHalf(reader), ReadHalf(reader), ReadHalf(reader), ReadHalf(reader)),
MdlFile.VertexType.UByte4 => ReadUByte4(reader),
MdlFile.VertexType.NByte4 => ReadUByte4(reader),
_ => Vector4.zero,
};
}
private static void WritePosition(MdlFile.VertexType type, Vector3d value, Span<byte> target)
{
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)
{
WriteVector3(type, value, target, normalized: type == MdlFile.VertexType.NByte4);
}
private static void WriteTangent(MdlFile.VertexType type, Vector4 value, Span<byte> target)
{
if (type == MdlFile.VertexType.NByte4)
{
WriteNByte4(value, target);
return;
}
WriteVector4(type, value, target);
}
private static void WriteColor(MdlFile.VertexType type, Vector4 value, Span<byte> target)
{
if (type == MdlFile.VertexType.Single4)
{
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)
{
return;
}
target[0] = (byte)Math.Clamp(weights.boneIndex0, 0, 255);
target[1] = (byte)Math.Clamp(weights.boneIndex1, 0, 255);
target[2] = (byte)Math.Clamp(weights.boneIndex2, 0, 255);
target[3] = (byte)Math.Clamp(weights.boneIndex3, 0, 255);
}
private static void WriteBlendWeights(MdlFile.VertexType type, BoneWeight weights, Span<byte> target)
{
if (type != MdlFile.VertexType.UByte4 && type != MdlFile.VertexType.NByte4)
{
if (type == MdlFile.VertexType.Single4)
{
BinaryPrimitives.WriteSingleLittleEndian(target[..4], weights.boneWeight0);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(4, 4), weights.boneWeight1);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(8, 4), weights.boneWeight2);
BinaryPrimitives.WriteSingleLittleEndian(target.Slice(12, 4), weights.boneWeight3);
}
return;
}
var w0 = Clamp01(weights.boneWeight0);
var w1 = Clamp01(weights.boneWeight1);
var w2 = Clamp01(weights.boneWeight2);
var w3 = Clamp01(weights.boneWeight3);
NormalizeWeights(ref w0, ref w1, ref w2, ref w3);
target[0] = ToByte(w0);
target[1] = ToByte(w1);
target[2] = ToByte(w2);
target[3] = ToByte(w3);
}
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)
{
var uv = uvChannels[mapping.FirstChannel][vertexIndex];
WriteVector2(type, uv, target);
return;
}
if (type == MdlFile.VertexType.Half4 || type == MdlFile.VertexType.Single4)
{
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.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);
}
}
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);
}
}
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;
}
}
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);
WriteUByte4(normalized, target);
}
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 byte ToByte(float value)
=> (byte)Math.Clamp((int)Math.Round(value * 255f), 0, 255);
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 int GetElementSize(MdlFile.VertexType type)
=> type switch
{
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,
_ => throw new InvalidOperationException($"Unsupported vertex type {type}"),
};
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? normalElement,
MdlStructs.VertexElement? tangentElement,
MdlStructs.VertexElement? colorElement,
MdlStructs.VertexElement? blendIndicesElement,
MdlStructs.VertexElement? blendWeightsElement,
List<UvElementPacking> uvElements,
int uvChannelCount)
{
SortedElements = sortedElements;
NormalElement = normalElement;
TangentElement = tangentElement;
ColorElement = colorElement;
BlendIndicesElement = blendIndicesElement;
BlendWeightsElement = blendWeightsElement;
UvElements = uvElements;
UvChannelCount = uvChannelCount;
}
public List<MdlStructs.VertexElement> SortedElements { get; }
public MdlStructs.VertexElement? NormalElement { get; }
public MdlStructs.VertexElement? TangentElement { 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 HasTangents => TangentElement.HasValue;
public bool HasColors => ColorElement.HasValue;
public bool HasSkinning => BlendIndicesElement.HasValue && BlendWeightsElement.HasValue;
}
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[]? colors,
BoneWeight[]? boneWeights,
Vector2[][]? uvChannels)
{
Positions = positions;
Normals = normals;
Tangents = tangents;
Colors = colors;
BoneWeights = boneWeights;
UvChannels = uvChannels;
}
public Vector3d[] Positions { get; }
public Vector3[]? Normals { get; }
public Vector4[]? Tangents { get; }
public Vector4[]? Colors { get; }
public BoneWeight[]? BoneWeights { get; }
public Vector2[][]? UvChannels { get; }
}
}
internal static class MeshDecimatorVectorExtensions
{
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];
}