Co-authored-by: azyges <aaaaaa@aaa.aaa> Co-authored-by: cake <admin@cakeandbanana.nl> Co-authored-by: defnotken <itsdefnotken@gmail.com> Reviewed-on: #131
1463 lines
53 KiB
C#
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];
|
|
}
|