using System.Globalization; using System.Numerics; using System.Text; namespace XFEEDToolBox.Core.Rendering; public readonly record struct MeshVertex(Vector3 Position, Vector3 Normal); public sealed record ShipMesh(MeshVertex[] Vertices, string Source, string Sha256) { public int Triangles => Vertices.Length / 3; } /// Local mesh reader. Geometry is validated before any GPU allocation. public static class ShipMeshReader { public const int MaximumTriangles = 2_000_000; public const long MaximumFileBytes = 256 * 1024 * 1024; public static ShipMesh Read(string path) { var file = new FileInfo(path); if (!file.Exists) throw new FileNotFoundException("飞船模型文件不存在。", path); if (file.Length is < 16 or > MaximumFileBytes) throw new InvalidDataException("模型文件大小超出限制。"); var bytes = File.ReadAllBytes(path); var vertices = Path.GetExtension(path).ToLowerInvariant() switch { ".obj" => ReadObj(Encoding.UTF8.GetString(bytes)), ".stl" => ReadStl(bytes), _ => throw new InvalidDataException("模型格式应为 OBJ 或 STL。") }; Normalize(vertices); return new(vertices, path, Convert.ToHexString(System.Security.Cryptography.SHA256.HashData(bytes))); } public static ShipMesh Create(MeshVertex[] triangles, string source, string sha256) { if (triangles.Length == 0 || triangles.Length % 3 != 0 || triangles.Length / 3 > MaximumTriangles) throw new InvalidDataException("船体三角面数量无效。"); foreach (var vertex in triangles) { Validate(vertex.Position); Validate(vertex.Normal); } Normalize(triangles); return new(triangles, source, sha256); } public static MeshVertex[] ReadObj(string text) { List positions = []; List normals = []; List output = []; using var reader = new StringReader(text); while (reader.ReadLine() is { } line) { var comment = line.IndexOf('#'); if (comment >= 0) line = line[..comment]; var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 0) continue; if (parts[0] == "v" && parts.Length >= 4) { if (positions.Count >= MaximumTriangles * 3) throw new InvalidDataException("模型顶点过多。"); positions.Add(Vector(parts)); } else if (parts[0] == "vn" && parts.Length >= 4) normals.Add(Vector(parts)); else if (parts[0] == "f" && parts.Length >= 4) { if (parts.Length > 4097) throw new InvalidDataException("模型多边形过大。"); (Vector3 Position, Vector3? Normal) Point(string face) { var indices = face.Split('/'); var p = Index(indices[0], positions.Count); Vector3? normal = indices.Length > 2 && indices[2].Length > 0 ? normals[Index(indices[2], normals.Count)] : null; return (positions[p], normal); } var first = Point(parts[1]); for (var i = 2; i + 1 < parts.Length; i++) { var second = Point(parts[i]); var third = Point(parts[i + 1]); AddTriangle(output, first.Position, second.Position, third.Position, first.Normal, second.Normal, third.Normal); } } } if (output.Count == 0) throw new InvalidDataException("模型没有可绘制的三角形。"); return output.ToArray(); } public static MeshVertex[] ReadStl(byte[] data) { List output = []; if (data.Length >= 84) { var count = System.Buffers.Binary.BinaryPrimitives.ReadUInt32LittleEndian(data.AsSpan(80, 4)); if (84L + count * 50L == data.Length) { if (count > MaximumTriangles) throw new InvalidDataException("模型三角形过多。"); using var reader = new BinaryReader(new MemoryStream(data, false)); reader.BaseStream.Position = 84; for (var i = 0; i < count; i++) { _ = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); var a = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); var b = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); var c = new Vector3(reader.ReadSingle(), reader.ReadSingle(), reader.ReadSingle()); reader.ReadUInt16(); AddTriangle(output, a, b, c); } if (output.Count == 0) throw new InvalidDataException("模型没有有效三角形。"); return output.ToArray(); } } using var lines = new StringReader(Encoding.ASCII.GetString(data)); List triangle = []; while (lines.ReadLine() is { } line) { var parts = line.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 4 && parts[0] == "vertex") { triangle.Add(Vector(parts)); if (triangle.Count == 3) { AddTriangle(output, triangle[0], triangle[1], triangle[2]); triangle.Clear(); } } } if (output.Count == 0 || triangle.Count != 0) throw new InvalidDataException("STL 模型不完整。"); return output.ToArray(); } private static int Index(string value, int count) { if (!int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var n) || n == 0) throw new InvalidDataException("OBJ 顶点索引无效。"); var result = n > 0 ? n - 1 : count + n; if ((uint)result >= count) throw new InvalidDataException("OBJ 顶点索引越界。"); return result; } private static Vector3 Vector(string[] parts) { if (!float.TryParse(parts[1], NumberStyles.Float, CultureInfo.InvariantCulture, out var x) || !float.TryParse(parts[2], NumberStyles.Float, CultureInfo.InvariantCulture, out var y) || !float.TryParse(parts[3], NumberStyles.Float, CultureInfo.InvariantCulture, out var z)) throw new InvalidDataException("模型坐标无效。"); var result = new Vector3(x, y, z); Validate(result); return result; } private static void Validate(Vector3 v) { if (!float.IsFinite(v.X) || !float.IsFinite(v.Y) || !float.IsFinite(v.Z) || v.LengthSquared() > 1e20f) throw new InvalidDataException("模型坐标超出范围。"); } private static Vector3 Normal(Vector3? given, Vector3 fallback) => given is { } n && float.IsFinite(n.LengthSquared()) && n.LengthSquared() > 1e-12f ? Vector3.Normalize(n) : fallback; private static void AddTriangle(List output, Vector3 a, Vector3 b, Vector3 c, Vector3? na = null, Vector3? nb = null, Vector3? nc = null) { Validate(a); Validate(b); Validate(c); var normal = Vector3.Cross(b - a, c - a); if (normal.LengthSquared() <= 1e-16f) return; normal = Vector3.Normalize(normal); if (output.Count / 3 >= MaximumTriangles) throw new InvalidDataException("模型三角形过多。"); output.Add(new(a, Normal(na, normal))); output.Add(new(b, Normal(nb, normal))); output.Add(new(c, Normal(nc, normal))); } private static void Normalize(MeshVertex[] vertices) { var min = new Vector3(float.PositiveInfinity); var max = new Vector3(float.NegativeInfinity); foreach (var v in vertices) { min = Vector3.Min(min, v.Position); max = Vector3.Max(max, v.Position); } var centre = (min + max) * .5f; var size = max - min; var span = Math.Max(size.X, Math.Max(size.Y, size.Z)); if (!float.IsFinite(span) || span <= 1e-8f) throw new InvalidDataException("模型尺寸无效。"); for (var i = 0; i < vertices.Length; i++) vertices[i] = vertices[i] with { Position = (vertices[i].Position - centre) * (2 / span) }; } }