mirror of
https://github.com/sampletext32/ParkanPlayground.git
synced 2026-08-15 02:57:49 +04:00
vibecoded inspector
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
using System.Numerics;
|
||||
using MshLib;
|
||||
using NResLib;
|
||||
using NResUI.Rendering.Inspection;
|
||||
using NResUI.Rendering.Materials;
|
||||
using NResUI.Rendering.Viewport.Msh;
|
||||
|
||||
namespace NResUI.Rendering.Import.Msh;
|
||||
|
||||
public static class MshMeshDocumentImporter
|
||||
{
|
||||
private const int DefaultModelState = 0;
|
||||
private const int DefaultLod = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Читает MSH/NRes с диска и строит CPU-документ для инспектора.
|
||||
/// OpenGL здесь намеренно не используется: этот слой должен быть проверяемым без viewport.
|
||||
/// </summary>
|
||||
public static MeshDocumentLoadResult LoadFromFile(string path)
|
||||
{
|
||||
var warnings = new List<string>();
|
||||
|
||||
try
|
||||
{
|
||||
using var fs = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
|
||||
var parseResult = NResParser.ReadFile(fs);
|
||||
if (parseResult.Archive == null)
|
||||
return MeshDocumentLoadResult.Failure(parseResult.Error ?? "Failed to parse MSH/NRes file.", path);
|
||||
|
||||
var archive = parseResult.Archive;
|
||||
var meshType = MshConverter.DetectMeshType(archive);
|
||||
if (meshType != MshType.Model)
|
||||
{
|
||||
return MeshDocumentLoadResult.Failure(
|
||||
$"Only normal MSH models are supported by the mesh inspector. Detected: {meshType}.",
|
||||
path);
|
||||
}
|
||||
|
||||
fs.Seek(0, SeekOrigin.Begin);
|
||||
var document = LoadModelDocument(fs, archive, path, warnings);
|
||||
return MeshDocumentLoadResult.Success(document);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return MeshDocumentLoadResult.Failure(ex.Message, path);
|
||||
}
|
||||
}
|
||||
|
||||
private static MeshDocument LoadModelDocument(
|
||||
FileStream fs,
|
||||
NResArchive archive,
|
||||
string path,
|
||||
List<string> warnings)
|
||||
{
|
||||
var nodes = Msh0x01.ReadComponent(fs, archive);
|
||||
var geometry = Msh0x02.ReadComponent(fs, archive);
|
||||
var positions = Msh0x03.ReadComponent(fs, archive);
|
||||
var normals = TryReadNormals(fs, archive, warnings);
|
||||
var uvs = TryReadUvs(fs, archive, warnings);
|
||||
var indices = Msh0x06.ReadComponent(fs, archive);
|
||||
var batches = Msh0x0D.ReadComponent(fs, archive);
|
||||
var animationDescriptors = Msh0x08.ReadComponent(fs, archive);
|
||||
var restPoses = MshRestPoseBuilder.BuildRestPose(nodes, animationDescriptors);
|
||||
var names = TryReadNames(fs, archive, warnings);
|
||||
|
||||
var weaMatch = WeaResourceResolver.TryLoadMatchingWeaForMsh(path, warnings);
|
||||
if (weaMatch != null)
|
||||
warnings.Add($"Loaded WEA '{Path.GetFileName(weaMatch.Path)}' ({weaMatch.Reason})");
|
||||
|
||||
var materialNamesById = weaMatch?.File.Materials.ToDictionary(x => x.Id, x => x.Name)
|
||||
?? new Dictionary<int, string>();
|
||||
|
||||
var pieces = new List<MeshPieceInfo>(nodes.Nodes.Count);
|
||||
for (var nodeIndex = 0; nodeIndex < nodes.Nodes.Count; nodeIndex++)
|
||||
{
|
||||
var node = nodes.Nodes[nodeIndex];
|
||||
var restPose = restPoses[nodeIndex];
|
||||
var activeSlotIndex = node.ResolveSlotIndex(DefaultModelState, DefaultLod);
|
||||
var boundsMin = Vector3.Zero;
|
||||
var boundsMax = Vector3.Zero;
|
||||
|
||||
if (activeSlotIndex != ushort.MaxValue && activeSlotIndex < geometry.Slots.Count)
|
||||
{
|
||||
var slot = geometry.Slots[activeSlotIndex];
|
||||
boundsMin = ToNumericsVector3(slot.LocalMinimum);
|
||||
boundsMax = ToNumericsVector3(slot.LocalMaximum);
|
||||
}
|
||||
|
||||
pieces.Add(new MeshPieceInfo
|
||||
{
|
||||
Id = nodeIndex,
|
||||
Name = ResolvePieceName(names, nodeIndex),
|
||||
ParentId = restPose.ParentIndex,
|
||||
Flags = node.Flags,
|
||||
GeometrySlotsByStateAndLod = node.Msh02SlotIndicesByStateAndLOD.ToArray(),
|
||||
Batches = BuildPieceBatchInfo(node, geometry, batches, indices.Count, materialNamesById, warnings),
|
||||
LocalTransform = restPose.LocalTransform,
|
||||
MeshSpaceTransform = restPose.MeshSpaceTransform,
|
||||
BoundsMin = boundsMin,
|
||||
BoundsMax = boundsMax,
|
||||
FallbackKeyframeIndex = restPose.FallbackKeyframeIndex,
|
||||
HasRestPose = restPose.HasFallbackPose
|
||||
});
|
||||
}
|
||||
|
||||
return new MeshDocument
|
||||
{
|
||||
SourcePath = path,
|
||||
MeshType = MshType.Model,
|
||||
Pieces = pieces,
|
||||
Materials = materialNamesById
|
||||
.Select(x => new MeshMaterialInfo { Id = x.Key, Name = x.Value })
|
||||
.OrderBy(x => x.Id)
|
||||
.ToList(),
|
||||
Warnings = warnings,
|
||||
MshModelGeometry = new MshModelGeometry
|
||||
{
|
||||
Nodes = nodes,
|
||||
GeometrySlots = geometry,
|
||||
Positions = positions,
|
||||
Normals = normals,
|
||||
Uvs = uvs,
|
||||
Indices = indices,
|
||||
Batches = batches
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Собирает batch metadata для всех state/LOD слотов piece.
|
||||
/// Дубликаты batch убираются, потому что разные состояния могут ссылаться на один и тот же geometry slot.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<MeshBatchInfo> BuildPieceBatchInfo(
|
||||
Msh0x01.Node node,
|
||||
Msh0x02.Msh0x02Component geometry,
|
||||
IReadOnlyList<Msh0x0D.Batch> batches,
|
||||
int indexCount,
|
||||
IReadOnlyDictionary<int, string> materialNamesById,
|
||||
ICollection<string> documentWarnings)
|
||||
{
|
||||
var result = new List<MeshBatchInfo>();
|
||||
var seenBatchIndices = new HashSet<int>();
|
||||
|
||||
foreach (var slotIndex in node.Msh02SlotIndicesByStateAndLOD.Distinct())
|
||||
{
|
||||
if (slotIndex == ushort.MaxValue)
|
||||
continue;
|
||||
|
||||
if (slotIndex >= geometry.Slots.Count)
|
||||
{
|
||||
documentWarnings.Add($"Geometry slot {slotIndex} is out of range.");
|
||||
continue;
|
||||
}
|
||||
|
||||
var slot = geometry.Slots[slotIndex];
|
||||
for (var batchIndex = slot.BatchStart0x0D; batchIndex < slot.BatchEndExclusive0x0D; batchIndex++)
|
||||
{
|
||||
if (batchIndex < 0 || batchIndex >= batches.Count || !seenBatchIndices.Add(batchIndex))
|
||||
continue;
|
||||
|
||||
var batch = batches[batchIndex];
|
||||
var warnings = new List<string>();
|
||||
if (batch.IndexStart0x06 + batch.IndexCount0x06 > indexCount)
|
||||
warnings.Add("Index range exceeds MSH 0x06 index buffer.");
|
||||
|
||||
var materialName = materialNamesById.GetValueOrDefault(batch.MaterialIndexLo);
|
||||
|
||||
if (materialName == "B_PLT_06")
|
||||
{
|
||||
_ = 5;
|
||||
}
|
||||
|
||||
result.Add(new MeshBatchInfo
|
||||
{
|
||||
BatchIndex = batchIndex,
|
||||
MaterialId = batch.MaterialIndexLo,
|
||||
MaterialName = materialName,
|
||||
Flags = batch.Flags,
|
||||
IndexStart = batch.IndexStart0x06,
|
||||
IndexCount = batch.IndexCount0x06,
|
||||
BaseVertex = batch.BaseVertex0x03,
|
||||
VertexCount = batch.VertexCount0x03,
|
||||
TriangleCount = batch.IndexCount0x06 / 3,
|
||||
Warnings = warnings
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static List<string> TryReadNames(FileStream fs, NResArchive archive, ICollection<string> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Msh0x0A.ReadComponent(fs, archive);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
warnings.Add($"MSH 0x0A names are unavailable: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Msh04Normal> TryReadNormals(FileStream fs, NResArchive archive, ICollection<string> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Msh0x04.ReadComponent(fs, archive);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
warnings.Add($"MSH 0x04 normals are unavailable: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static List<Msh05Uv> TryReadUvs(FileStream fs, NResArchive archive, ICollection<string> warnings)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Msh0x05.ReadComponent(fs, archive);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
warnings.Add($"MSH 0x05 UVs are unavailable: {ex.Message}");
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
private static string ResolvePieceName(IReadOnlyList<string> names, int nodeIndex)
|
||||
{
|
||||
if (nodeIndex >= 0 && nodeIndex < names.Count && !string.IsNullOrWhiteSpace(names[nodeIndex]))
|
||||
return $"{nodeIndex}: {names[nodeIndex]}";
|
||||
|
||||
return $"{nodeIndex}: piece_{nodeIndex:D3}";
|
||||
}
|
||||
|
||||
private static Vector3 ToNumericsVector3(Common.Vector3 position)
|
||||
{
|
||||
return new Vector3(position.X, position.Y, position.Z);
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class MeshDocumentLoadResult
|
||||
{
|
||||
public bool IsSuccess { get; }
|
||||
public string? Error { get; }
|
||||
public string? SourcePath { get; }
|
||||
public MeshDocument? Document { get; }
|
||||
|
||||
private MeshDocumentLoadResult(bool isSuccess, string? error, string? sourcePath, MeshDocument? document)
|
||||
{
|
||||
IsSuccess = isSuccess;
|
||||
Error = error;
|
||||
SourcePath = sourcePath;
|
||||
Document = document;
|
||||
}
|
||||
|
||||
public static MeshDocumentLoadResult Success(MeshDocument document)
|
||||
{
|
||||
return new MeshDocumentLoadResult(true, null, document.SourcePath, document);
|
||||
}
|
||||
|
||||
public static MeshDocumentLoadResult Failure(string error, string? sourcePath = null)
|
||||
{
|
||||
return new MeshDocumentLoadResult(false, error, sourcePath, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
using System.Numerics;
|
||||
using MshLib;
|
||||
using NResUI.Rendering.Inspection;
|
||||
using NResUI.Rendering.Materials;
|
||||
using NResUI.Rendering.Viewport;
|
||||
using NResUI.Rendering.Viewport.Meshes;
|
||||
using Silk.NET.OpenGL;
|
||||
|
||||
namespace NResUI.Rendering.Import.Msh;
|
||||
|
||||
public static class MshViewportMeshFactory
|
||||
{
|
||||
/// <summary>
|
||||
/// Строит OpenGL-меши из уже разобранного MeshDocument.
|
||||
/// State/LOD выбираются отдельно для каждого piece через 0x01, а не через общий список geometry slots.
|
||||
/// </summary>
|
||||
public static IReadOnlyList<ViewportPiece> BuildViewportPieces(
|
||||
GL gl,
|
||||
MeshDocument document,
|
||||
MeshRenderState renderState,
|
||||
ViewportMaterialSet materialSet)
|
||||
{
|
||||
if (document.MshModelGeometry == null)
|
||||
return [];
|
||||
|
||||
var result = new List<ViewportPiece>();
|
||||
var modelGeometry = document.MshModelGeometry;
|
||||
var mshToViewportTransform = Matrix4x4.CreateRotationX(-MathF.PI * 0.5f);
|
||||
|
||||
foreach (var pieceInfo in document.Pieces)
|
||||
{
|
||||
if (!renderState.IsPieceVisible(pieceInfo.Id))
|
||||
continue;
|
||||
|
||||
var (pieceState, pieceLod) = ResolvePieceStateLod(document, pieceInfo, renderState);
|
||||
var slotIndex = pieceInfo.ResolveSlotIndex(pieceState, pieceLod);
|
||||
if (slotIndex == ushort.MaxValue || slotIndex >= modelGeometry.GeometrySlots.Slots.Count)
|
||||
{
|
||||
if (renderState.ShowEmptyPieces)
|
||||
AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "No geometry slot for selected state/LOD");
|
||||
continue;
|
||||
}
|
||||
|
||||
var slot = modelGeometry.GeometrySlots.Slots[slotIndex];
|
||||
if (!slot.HasBatches)
|
||||
{
|
||||
if (renderState.ShowEmptyPieces)
|
||||
AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "Geometry slot has no batches", slotIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
var meshBuildResult = BuildPieceMeshes(
|
||||
gl,
|
||||
pieceInfo.Id,
|
||||
slot,
|
||||
modelGeometry,
|
||||
renderState,
|
||||
materialSet);
|
||||
|
||||
if (meshBuildResult == null)
|
||||
{
|
||||
if (renderState.ShowEmptyPieces)
|
||||
AddEmptyPieceMarker(gl, result, pieceInfo, mshToViewportTransform, "Geometry slot produced no valid triangles", slotIndex);
|
||||
continue;
|
||||
}
|
||||
|
||||
result.Add(new ViewportPiece(
|
||||
id: pieceInfo.Id,
|
||||
name: pieceInfo.Name,
|
||||
meshes: meshBuildResult.Meshes,
|
||||
localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform,
|
||||
boundsMin: meshBuildResult.BoundsMin,
|
||||
boundsMax: meshBuildResult.BoundsMax,
|
||||
debugInfo: new ViewportPieceDebugInfo
|
||||
{
|
||||
SourceKind = "MSH 0x01 piece",
|
||||
SourcePieceIndex = pieceInfo.Id,
|
||||
SourceParentIndex = pieceInfo.ParentId,
|
||||
GeometrySlotIndex = slotIndex,
|
||||
Msh01Flags = (uint)pieceInfo.Flags,
|
||||
BatchCount = meshBuildResult.BatchCount,
|
||||
TriangleCount = meshBuildResult.TriangleCount,
|
||||
FallbackKeyframeIndex = pieceInfo.FallbackKeyframeIndex,
|
||||
HasRestPose = pieceInfo.HasRestPose
|
||||
},
|
||||
sourceBatchIndices: meshBuildResult.BatchIndices));
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static (int State, int Lod) ResolvePieceStateLod(
|
||||
MeshDocument document,
|
||||
MeshPieceInfo pieceInfo,
|
||||
MeshRenderState renderState)
|
||||
{
|
||||
if (renderState.PieceStateOverrides.ContainsKey(pieceInfo.Id) ||
|
||||
renderState.PieceLodOverrides.ContainsKey(pieceInfo.Id))
|
||||
{
|
||||
return (renderState.GetPieceState(pieceInfo.Id), renderState.GetPieceLod(pieceInfo.Id));
|
||||
}
|
||||
|
||||
// Без явного override piece использует первую реально присутствующую пару state/LOD из своей 0x01 таблицы.
|
||||
for (var state = 0; state < document.ModelStateCount; state++)
|
||||
{
|
||||
for (var lod = 0; lod < document.LodCount; lod++)
|
||||
{
|
||||
var slot = pieceInfo.ResolveSlotIndex(state, lod);
|
||||
if (slot != ushort.MaxValue &&
|
||||
(document.MshModelGeometry == null || slot < document.MshModelGeometry.GeometrySlots.Slots.Count))
|
||||
{
|
||||
return (state, lod);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (0, 0);
|
||||
}
|
||||
|
||||
private static void AddEmptyPieceMarker(
|
||||
GL gl,
|
||||
List<ViewportPiece> pieces,
|
||||
MeshPieceInfo pieceInfo,
|
||||
Matrix4x4 mshToViewportTransform,
|
||||
string reason,
|
||||
int geometrySlotIndex = -1)
|
||||
{
|
||||
// Пустые pieces все равно важны: это могут быть сокеты, логические узлы или placeholders поврежденных состояний.
|
||||
var marker = PrimitiveMeshes.CreateAxes(gl, 0.35f);
|
||||
pieces.Add(new ViewportPiece(
|
||||
id: pieceInfo.Id,
|
||||
name: pieceInfo.Name,
|
||||
mesh: marker,
|
||||
localTransform: pieceInfo.MeshSpaceTransform * mshToViewportTransform,
|
||||
boundsMin: new Vector3(-0.35f, -0.35f, -0.35f),
|
||||
boundsMax: new Vector3(0.35f, 0.35f, 0.35f),
|
||||
debugInfo: new ViewportPieceDebugInfo
|
||||
{
|
||||
SourceKind = reason,
|
||||
SourcePieceIndex = pieceInfo.Id,
|
||||
SourceParentIndex = pieceInfo.ParentId,
|
||||
GeometrySlotIndex = geometrySlotIndex,
|
||||
Msh01Flags = (uint)pieceInfo.Flags,
|
||||
BatchCount = 0,
|
||||
TriangleCount = 0,
|
||||
FallbackKeyframeIndex = pieceInfo.FallbackKeyframeIndex,
|
||||
HasRestPose = pieceInfo.HasRestPose
|
||||
}));
|
||||
}
|
||||
|
||||
private static PieceMeshBuildResult? BuildPieceMeshes(
|
||||
GL gl,
|
||||
int pieceId,
|
||||
Msh0x02.GeometrySlot slot,
|
||||
MshModelGeometry modelGeometry,
|
||||
MeshRenderState renderState,
|
||||
ViewportMaterialSet materialSet)
|
||||
{
|
||||
var meshes = new List<GpuMesh>();
|
||||
var batchIndices = new List<int>();
|
||||
var debugColor = PickDebugColor(pieceId);
|
||||
var batchCount = 0;
|
||||
var triangleCount = 0;
|
||||
|
||||
var boundsMin = new Vector3(float.PositiveInfinity, float.PositiveInfinity, float.PositiveInfinity);
|
||||
var boundsMax = new Vector3(float.NegativeInfinity, float.NegativeInfinity, float.NegativeInfinity);
|
||||
|
||||
for (var batchIndex = slot.BatchStart0x0D; batchIndex < slot.BatchEndExclusive0x0D; batchIndex++)
|
||||
{
|
||||
if (batchIndex < 0 || batchIndex >= modelGeometry.Batches.Count || !renderState.IsBatchVisible(batchIndex))
|
||||
continue;
|
||||
|
||||
var batch = modelGeometry.Batches[batchIndex];
|
||||
if (batch.IndexStart0x06 + batch.IndexCount0x06 > modelGeometry.Indices.Count)
|
||||
continue;
|
||||
|
||||
var vertices = new List<float>();
|
||||
var outIndices = new List<uint>();
|
||||
var batchTriangleCount = 0;
|
||||
|
||||
var material = materialSet.FindMaterial(batch.MaterialIndexLo) ?? ViewportMaterial.Untextured;
|
||||
var vertexColor = material.HasTexture ? Vector3.One : debugColor;
|
||||
|
||||
for (var i = 0; i + 2 < batch.IndexCount0x06; i += 3)
|
||||
{
|
||||
var indexBase = (int)batch.IndexStart0x06 + i;
|
||||
var vertexIndex0 = checked((int)batch.BaseVertex0x03 + modelGeometry.Indices[indexBase + 0]);
|
||||
var vertexIndex1 = checked((int)batch.BaseVertex0x03 + modelGeometry.Indices[indexBase + 1]);
|
||||
var vertexIndex2 = checked((int)batch.BaseVertex0x03 + modelGeometry.Indices[indexBase + 2]);
|
||||
|
||||
if (!IsValidTriangle(modelGeometry.Positions.Count, vertexIndex0, vertexIndex1, vertexIndex2))
|
||||
continue;
|
||||
|
||||
var p0 = ToNumericsVector3(modelGeometry.Positions[vertexIndex0]);
|
||||
var p1 = ToNumericsVector3(modelGeometry.Positions[vertexIndex1]);
|
||||
var p2 = ToNumericsVector3(modelGeometry.Positions[vertexIndex2]);
|
||||
|
||||
var fallbackNormal = BuildFaceNormal(p0, p1, p2);
|
||||
|
||||
AddTriangleVertex(p0, vertexColor, GetNormal(modelGeometry.Normals, vertexIndex0, fallbackNormal, renderState.UseStoredNormals), GetUv(modelGeometry.Uvs, vertexIndex0), vertices, outIndices, ref boundsMin, ref boundsMax);
|
||||
AddTriangleVertex(p1, vertexColor, GetNormal(modelGeometry.Normals, vertexIndex1, fallbackNormal, renderState.UseStoredNormals), GetUv(modelGeometry.Uvs, vertexIndex1), vertices, outIndices, ref boundsMin, ref boundsMax);
|
||||
AddTriangleVertex(p2, vertexColor, GetNormal(modelGeometry.Normals, vertexIndex2, fallbackNormal, renderState.UseStoredNormals), GetUv(modelGeometry.Uvs, vertexIndex2), vertices, outIndices, ref boundsMin, ref boundsMax);
|
||||
batchTriangleCount++;
|
||||
}
|
||||
|
||||
if (batchTriangleCount == 0)
|
||||
continue;
|
||||
|
||||
batchCount++;
|
||||
batchIndices.Add(batchIndex);
|
||||
triangleCount += batchTriangleCount;
|
||||
meshes.Add(PrimitiveMeshes.CreateColoredIndexedMesh(gl, vertices, outIndices, PrimitiveType.Triangles, material));
|
||||
}
|
||||
|
||||
if (triangleCount == 0 || meshes.Count == 0)
|
||||
return null;
|
||||
|
||||
return new PieceMeshBuildResult(meshes, boundsMin, boundsMax, batchCount, triangleCount, batchIndices);
|
||||
}
|
||||
|
||||
private static Vector3 BuildFaceNormal(Vector3 p0, Vector3 p1, Vector3 p2)
|
||||
{
|
||||
var normal = Vector3.Cross(p1 - p0, p2 - p0);
|
||||
return normal.LengthSquared() < 1e-8f ? Vector3.UnitY : Vector3.Normalize(normal);
|
||||
}
|
||||
|
||||
private static Vector3 GetNormal(
|
||||
IReadOnlyList<Msh04Normal> normals,
|
||||
int vertexIndex,
|
||||
Vector3 fallbackNormal,
|
||||
bool useStoredNormals)
|
||||
{
|
||||
if (!useStoredNormals || vertexIndex < 0 || vertexIndex >= normals.Count)
|
||||
return fallbackNormal;
|
||||
|
||||
var packed = normals[vertexIndex];
|
||||
var normal = new Vector3(
|
||||
Math.Clamp(packed.X / 127.0f, -1.0f, 1.0f),
|
||||
Math.Clamp(packed.Y / 127.0f, -1.0f, 1.0f),
|
||||
Math.Clamp(packed.Z / 127.0f, -1.0f, 1.0f));
|
||||
|
||||
// У битых/нулевых normal entries оставляем face normal, чтобы освещение не превращалось в NaN.
|
||||
return normal.LengthSquared() < 1e-8f ? fallbackNormal : Vector3.Normalize(normal);
|
||||
}
|
||||
|
||||
private static Vector2 GetUv(IReadOnlyList<Msh05Uv> uvs, int vertexIndex)
|
||||
{
|
||||
if (vertexIndex < 0 || vertexIndex >= uvs.Count)
|
||||
return Vector2.Zero;
|
||||
|
||||
var uv = uvs[vertexIndex];
|
||||
return new Vector2(uv.U / 1024.0f, uv.V / 1024.0f);
|
||||
}
|
||||
|
||||
private static Vector3 ToNumericsVector3(Common.Vector3 position)
|
||||
{
|
||||
return new Vector3(position.X, position.Y, position.Z);
|
||||
}
|
||||
|
||||
private static void AddTriangleVertex(
|
||||
Vector3 position,
|
||||
Vector3 color,
|
||||
Vector3 normal,
|
||||
Vector2 uv,
|
||||
List<float> vertices,
|
||||
List<uint> indices,
|
||||
ref Vector3 boundsMin,
|
||||
ref Vector3 boundsMax)
|
||||
{
|
||||
var vertexIndex = (uint)(vertices.Count / PrimitiveMeshes.FloatsPerVertex);
|
||||
|
||||
vertices.Add(position.X);
|
||||
vertices.Add(position.Y);
|
||||
vertices.Add(position.Z);
|
||||
vertices.Add(color.X);
|
||||
vertices.Add(color.Y);
|
||||
vertices.Add(color.Z);
|
||||
vertices.Add(normal.X);
|
||||
vertices.Add(normal.Y);
|
||||
vertices.Add(normal.Z);
|
||||
vertices.Add(uv.X);
|
||||
vertices.Add(uv.Y);
|
||||
|
||||
indices.Add(vertexIndex);
|
||||
boundsMin = Vector3.Min(boundsMin, position);
|
||||
boundsMax = Vector3.Max(boundsMax, position);
|
||||
}
|
||||
|
||||
private static bool IsValidTriangle(int vertexCount, int vertexIndex0, int vertexIndex1, int vertexIndex2)
|
||||
{
|
||||
return IsValidVertexIndex(vertexCount, vertexIndex0) &&
|
||||
IsValidVertexIndex(vertexCount, vertexIndex1) &&
|
||||
IsValidVertexIndex(vertexCount, vertexIndex2);
|
||||
}
|
||||
|
||||
private static bool IsValidVertexIndex(int vertexCount, int vertexIndex)
|
||||
{
|
||||
return vertexIndex >= 0 && vertexIndex < vertexCount;
|
||||
}
|
||||
|
||||
private static Vector3 PickDebugColor(int index)
|
||||
{
|
||||
ReadOnlySpan<Vector3> palette =
|
||||
[
|
||||
new Vector3(0.90f, 0.36f, 0.30f),
|
||||
new Vector3(0.30f, 0.68f, 0.95f),
|
||||
new Vector3(0.42f, 0.82f, 0.42f),
|
||||
new Vector3(0.95f, 0.78f, 0.32f),
|
||||
new Vector3(0.78f, 0.48f, 0.95f),
|
||||
new Vector3(0.36f, 0.86f, 0.78f),
|
||||
new Vector3(0.95f, 0.55f, 0.78f),
|
||||
new Vector3(0.72f, 0.72f, 0.72f),
|
||||
];
|
||||
|
||||
return palette[index % palette.Length];
|
||||
}
|
||||
|
||||
private sealed record PieceMeshBuildResult(
|
||||
IReadOnlyList<GpuMesh> Meshes,
|
||||
Vector3 BoundsMin,
|
||||
Vector3 BoundsMax,
|
||||
int BatchCount,
|
||||
int TriangleCount,
|
||||
IReadOnlyList<int> BatchIndices);
|
||||
}
|
||||
Reference in New Issue
Block a user