Files
2026-07-13 13:13:17 +08:00

454 lines
17 KiB
C#

#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<Vector3> Positions = [];
public readonly List<Vector4> Colors = [];
public readonly List<Vector3> Normals = [];
public readonly List<Vector2> TexCoords = [];
public readonly List<Face> Faces = [];
public readonly List<Line> 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<Vertex> 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;
}
}
/// <summary>
/// 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.
/// </summary>
private List<Vertex> 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<Vertex>(count);
var reorderedTangents = new List<Vector3>(count);
var reorderedBinormals = new List<Vector3>(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);
}
/// <summary>
/// Vertex sorting is implement through an index look-up table
/// </summary>
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<Vertex>? _distinctVertices;
public readonly List<Vector3> VertexTangents = [];
public readonly List<Vector3> VertexBinormals = [];
public List<int> SortedVertexIndices=[];
private readonly Dictionary<long, int> _vertexIndicesByHash = new();
#endregion
}