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