using Lumina.Data.Parsing; using Lumina.Extensions; using MeshDecimator; 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 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 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(mdl.SubMeshes.Length); var newVertexBuffer = new List(mdl.VertexBufferSize[lodIndex] > 0 ? (int)mdl.VertexBufferSize[lodIndex] : 0); var newIndexBuffer = new List(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 (meshIndex >= lodMeshStart && meshIndex < lodMeshEnd && TryProcessMesh(mdl, lodIndex, meshIndex, mesh, meshSubMeshes, triangleThreshold, targetRatio, 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.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, 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 = MeshDecimation.CreateAlgorithm(Algorithm.Default); algorithm.Logger = 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 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(); 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(originalSubMeshes.Length); var indexList = new List(); 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 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((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 elements, out List 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 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 target) { WriteVector3(type, value, target, normalized: type == MdlFile.VertexType.NByte4); } private static void WriteTangent(MdlFile.VertexType type, Vector4 value, Span target) { if (type == MdlFile.VertexType.NByte4) { WriteNByte4(value, target); return; } WriteVector4(type, value, target); } private static void WriteColor(MdlFile.VertexType type, Vector4 value, Span target) { if (type == MdlFile.VertexType.Single4) { WriteVector4(type, value, target); return; } WriteUByte4(value, target); } private static void WriteBlendIndices(MdlFile.VertexType type, BoneWeight weights, Span 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 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 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 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 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 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 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 target) { var normalized = (value * 0.5f) + new Vector4(0.5f); WriteUByte4(normalized, target); } private static void WriteHalf(Span 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 sortedElements, MdlStructs.VertexElement? normalElement, MdlStructs.VertexElement? tangentElement, MdlStructs.VertexElement? colorElement, MdlStructs.VertexElement? blendIndicesElement, MdlStructs.VertexElement? blendWeightsElement, List uvElements, int uvChannelCount) { SortedElements = sortedElements; NormalElement = normalElement; TangentElement = tangentElement; ColorElement = colorElement; BlendIndicesElement = blendIndicesElement; BlendWeightsElement = blendWeightsElement; UvElements = uvElements; UvChannelCount = uvChannelCount; } public List 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 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]; }