#nullable enable using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.IO; using System.Linq; using T3.Core.Logging; // ReSharper disable InconsistentNaming // ReSharper disable RedundantNameQualifier namespace T3.Core.Rendering; public sealed class ObjMesh { public readonly List Positions = []; public readonly List Colors = []; public readonly List Normals = []; public readonly List TexCoords = []; public readonly List Faces = []; public readonly List Lines = []; public static bool TryLoadFromFile(string? objFilePath, [NotNullWhen(true)]out ObjMesh? mesh) { mesh = null; try { if (string.IsNullOrEmpty(objFilePath) || !new FileInfo(objFilePath).Exists) return false; } catch (Exception e) { Log.Warning("Failed to load object path:" + objFilePath + "\\n" + e); return false; } mesh = new ObjMesh(); var line = ""; try { using var stream = new StreamReader(objFilePath); while ((line = stream.ReadLine()) != null) { var lineEntries = line.Split(' '); switch (lineEntries[0]) { case "v": { var x = float.Parse(lineEntries[1], CultureInfo.InvariantCulture); var y = float.Parse(lineEntries[2], CultureInfo.InvariantCulture); var z = float.Parse(lineEntries[3], CultureInfo.InvariantCulture); mesh.Positions.Add(new Vector3(x, y, z)); var vertexIncludesColor = lineEntries.Length == 7; if (vertexIncludesColor) { var r = float.Parse(lineEntries[4], CultureInfo.InvariantCulture); var g = float.Parse(lineEntries[5], CultureInfo.InvariantCulture); var b = float.Parse(lineEntries[6], CultureInfo.InvariantCulture); mesh.Colors.Add(new Vector4(r, g, b, 1)); } break; } case "vt": { var u = float.Parse(lineEntries[1], CultureInfo.InvariantCulture); var v = float.Parse(lineEntries[2], CultureInfo.InvariantCulture); mesh.TexCoords.Add(new Vector2(u, v)); break; } case "vn": { var x = float.Parse(lineEntries[1], CultureInfo.InvariantCulture); var y = float.Parse(lineEntries[2], CultureInfo.InvariantCulture); var z = float.Parse(lineEntries[3], CultureInfo.InvariantCulture); mesh.Normals.Add(new Vector3(x, y, z)); break; } case "f": { SplitFaceIndices(lineEntries[1], out var v0V, out var v0T, out var v0N); SplitFaceIndices(lineEntries[2], out var v1V, out var v1T, out var v1N); SplitFaceIndices(lineEntries[3], out var v2V, out var v2T, out var v2N); mesh.Faces.Add(new Face( v0V, v0N, v0T, v1V, v1N, v1T, v2V, v2N, v2T)); if (lineEntries.Length > 4) { SplitFaceIndices(lineEntries[4], out var v3V, out var v3T, out var v3N); mesh.Faces.Add(new Face( v0V, v0N, v0T, v2V, v2N, v2T, v3V, v3N, v3T )); } break; } case "l": { mesh.Lines.Add(new Line(int.Parse(lineEntries[1], CultureInfo.InvariantCulture) - 1, int.Parse(lineEntries[2], CultureInfo.InvariantCulture) - 1 )); break; } } } } catch (Exception e) { Log.Error($"Failed to load obj cloud:{e.Message} '{line}'"); return false; } if (mesh.Colors.Count > 0 && mesh.Colors.Count != mesh.Positions.Count) { Log.Warning("Optional OBJ color information not defined for all vertices"); } return mesh.DistinctDistinctVertices.Count != 0; } private static void SplitFaceIndices(string v0, out int positionIndex, out int textureIndex, out int normalIndex) { var v0Entries = v0.Split('/'); positionIndex = int.Parse(v0Entries[0], CultureInfo.InvariantCulture) - 1; if (string.IsNullOrEmpty(v0Entries[1])) { textureIndex = 0; normalIndex = int.Parse(v0Entries[2], CultureInfo.InvariantCulture) - 1; } else { textureIndex = int.Parse(v0Entries[1], CultureInfo.InvariantCulture) - 1; normalIndex = int.Parse(v0Entries[2], CultureInfo.InvariantCulture) - 1; } } public struct Face { internal Face(int v0, int v0N, int v0T, int v1, int v1N, int v1T, int v2, int v2N, int v2T) { V0 = v0; V0n = v0N; V0t = v0T; V1 = v1; V1n = v1N; V1t = v1T; V2 = v2; V2n = v2N; V2t = v2T; } public readonly int V0; public readonly int V0n; public int V0t; public readonly int V1; public readonly int V1n; public int V1t; public readonly int V2; public readonly int V2n; public int V2t; } public readonly struct Line { internal Line(int v0, int v2) { V0 = v0; V2 = v2; } public readonly int V0; public readonly int V2; } #region joining vertices public List DistinctDistinctVertices { get { _distinctVertices ??= InitializeVertices(); return _distinctVertices; } } public int GetVertexIndex(int positionIndex, int normalIndex, int textureCoordsIndex) { var hash = Vertex.GetHashForIndices(positionIndex, normalIndex, textureCoordsIndex); if (_vertexIndicesByHash.TryGetValue(hash, out var index)) { return index; } return -1; } public readonly struct Vertex { public readonly int PositionIndex; public readonly int NormalIndex; public readonly int TextureCoordsIndex; internal Vertex(int positionIndex, int normalIndex, int textureCoordsIndex) { PositionIndex = positionIndex; NormalIndex = normalIndex; TextureCoordsIndex = textureCoordsIndex; } /*** * The hash is done by bit-shifting. This results in a maximum * vertex count of 64/3 bit = 2^21 = 2,097,152 vertices (!) */ internal static long GetHashForIndices(int pos, int normal, int textureCoords) { return (long)pos << 42 | (long)normal << 21 | (long)textureCoords; } } /// /// Tooll's mesh format uses vertex indices that combine all required information. /// This means that we have to split the OBJ-vertices if they use different normals or UVs. /// We do this by iterating over all face vertices, and generating a hash for face-, normal- and uv-index. /// If the hash already exists, we reuse and thus "merge the vertex" I.e. use it for different faces. /// private List InitializeVertices() { if (TexCoords.Count == 0) { TexCoords.Add(Vector2.Zero); } // compute fallback UVs as basis for TBN calculation... for (var index = 0; index < Faces.Count; index++) { var face = Faces[index]; var uv0 = TexCoords[face.V0t]; var uv1 = TexCoords[face.V1t]; var uv2 = TexCoords[face.V2t]; var needToGenerateUv = uv0 == Vector2.Zero && uv1 == Vector2.Zero && uv2 == Vector2.Zero; if (!needToGenerateUv) continue; var n0 = Normals[face.V0n]; var n1 = Normals[face.V1n]; var n2 = Normals[face.V2n]; var p0 = Positions[face.V0]; var p1 = Positions[face.V1]; var p2 = Positions[face.V2]; uv0 = GetUvFromPositionAndNormal(p0, n0); face.V0t = TexCoords.Count; TexCoords.Add(uv0); uv1 = GetUvFromPositionAndNormal(p1, n1); face.V1t = TexCoords.Count; TexCoords.Add(uv1); uv2 = GetUvFromPositionAndNormal(p2, n2); face.V2t = TexCoords.Count; TexCoords.Add(uv2); Faces[index] = face; } _distinctVertices = []; for (var faceIndex = 0; faceIndex < Faces.Count; faceIndex++) { if (faceIndex >= Faces.Count) { Log.Warning($"Skipping out of range {faceIndex} >= {Faces.Count} face index"); faceIndex = 0; } var face = Faces[faceIndex]; SortInMergedVertex(face.V0, face.V0n, face.V0t, faceIndex); SortInMergedVertex(face.V1, face.V1n, face.V1t, faceIndex); SortInMergedVertex(face.V2, face.V2n, face.V2t, faceIndex); } // Re-sort distinct vertices by their OBJ position index so that // SortedVertexIndices[i] == i preserves the original "v" line order. var count = _distinctVertices.Count; var sortedIndices = new int[count]; for (var i = 0; i < count; i++) sortedIndices[i] = i; Array.Sort(sortedIndices, (a, b) => _distinctVertices[a].PositionIndex.CompareTo(_distinctVertices[b].PositionIndex)); var reordered = new List(count); var reorderedTangents = new List(count); var reorderedBinormals = new List(count); for (var i = 0; i < count; i++) { var oldIndex = sortedIndices[i]; reordered.Add(_distinctVertices[oldIndex]); reorderedTangents.Add(VertexTangents[oldIndex]); reorderedBinormals.Add(VertexBinormals[oldIndex]); } // Rebuild the hash→index map with new positions _vertexIndicesByHash.Clear(); for (var i = 0; i < count; i++) { var v = reordered[i]; _vertexIndicesByHash[Vertex.GetHashForIndices(v.PositionIndex, v.NormalIndex, v.TextureCoordsIndex)] = i; } _distinctVertices.Clear(); _distinctVertices.AddRange(reordered); VertexTangents.Clear(); VertexTangents.AddRange(reorderedTangents); VertexBinormals.Clear(); VertexBinormals.AddRange(reorderedBinormals); return _distinctVertices; } private int SortInMergedVertex(int posIndex, int normalIndex, int texCoordIndex, int faceIndex) { if(_distinctVertices == null) return 0; var face = Faces[faceIndex]; var vertHash = Vertex.GetHashForIndices(posIndex, normalIndex, texCoordIndex); if (_vertexIndicesByHash.TryGetValue(vertHash, out var index)) { return index; } var p0 = Positions[face.V0]; var p1 = Positions[face.V1]; var p2 = Positions[face.V2]; var uv0 = TexCoords[face.V0t]; var uv1 = TexCoords[face.V1t]; var uv2 = TexCoords[face.V2t]; MeshUtils.CalcTBNSpace(p0: p0, uv0: uv0, p1: p1, uv1: uv1, p2: p2, uv2: uv2, normal: Normals[normalIndex], tangent: out var tangent, bitangent: out var bitangent); var newIndex = _distinctVertices.Count; var vert = new Vertex(posIndex, normalIndex, texCoordIndex); _vertexIndicesByHash[vertHash] = newIndex; VertexBinormals.Add(bitangent); VertexTangents.Add(tangent); _distinctVertices.Add(vert); return newIndex; } private static Vector2 GetUvFromPositionAndNormal(Vector3 pos, Vector3 normal) { var ax = MathF.Abs(normal.X); var ay = MathF.Abs(normal.Y); var az = MathF.Abs(normal.Z); if (ax > ay) { return ax > az ? new Vector2(pos.Y, pos.Z) : new Vector2(pos.X, pos.Y); } return ay > az ? new Vector2(pos.X, pos.Z) : new Vector2(pos.X, pos.Y); } /// /// Vertex sorting is implement through an index look-up table /// public void UpdateVertexSorting(SortDirections sortDirection) { if (_distinctVertices == null || _distinctVertices.Count == 0) { SortedVertexIndices = []; return; } SortedVertexIndices = Enumerable.Range(0, _distinctVertices.Count).ToList(); switch (sortDirection) { case SortDirections.XForward: SortedVertexIndices .Sort((v1, v2) => Positions[_distinctVertices[v1].PositionIndex].X.CompareTo(Positions[_distinctVertices[v2].PositionIndex].X)); break; case SortDirections.XBackwards: SortedVertexIndices.Sort((v1, v2) => Positions[_distinctVertices[v2].PositionIndex].X .CompareTo(Positions[_distinctVertices[v1].PositionIndex] .X)); break; case SortDirections.YForward: SortedVertexIndices.Sort((v1, v2) => Positions[_distinctVertices[v1].PositionIndex].Y .CompareTo(Positions[_distinctVertices[v2].PositionIndex] .Y)); break; case SortDirections.YBackwards: SortedVertexIndices.Sort((v1, v2) => Positions[_distinctVertices[v2].PositionIndex].Y .CompareTo(Positions[_distinctVertices[v1].PositionIndex] .Y)); break; case SortDirections.ZForward: SortedVertexIndices.Sort((v1, v2) => Positions[_distinctVertices[v1].PositionIndex].Z .CompareTo(Positions[_distinctVertices[v2].PositionIndex] .Z)); break; case SortDirections.ZBackwards: SortedVertexIndices.Sort((v1, v2) => Positions[_distinctVertices[v2].PositionIndex].Z .CompareTo(Positions[_distinctVertices[v1].PositionIndex] .Z)); break; case SortDirections.Ignore: break; } } public enum SortDirections { XForward, XBackwards, YForward, YBackwards, ZForward, ZBackwards, Ignore, } private List? _distinctVertices; public readonly List VertexTangents = []; public readonly List VertexBinormals = []; public List SortedVertexIndices=[]; private readonly Dictionary _vertexIndicesByHash = new(); #endregion }