|
|
|
@ -10,92 +10,120 @@ namespace SparkClient.Views.UserControl.ViewportData.Helper; |
|
|
|
|
public class ObjExporter |
|
|
|
|
{ |
|
|
|
|
public static void ExportToObj2(List<Viewport3DTriangleEntity> entities, string outputPath) |
|
|
|
|
{ |
|
|
|
|
// 分组:按面代码 PlaneCode 分组 |
|
|
|
|
var faceGroups = entities |
|
|
|
|
.GroupBy(e => e.PlaneCode) |
|
|
|
|
.ToDictionary(g => g.Key, g => g.ToList()); |
|
|
|
|
{ |
|
|
|
|
string obj = GenerateObj(entities); |
|
|
|
|
// Step 2: 写入 OBJ 文件 |
|
|
|
|
using (var writer = new StreamWriter(outputPath)) |
|
|
|
|
{ |
|
|
|
|
writer.Write(obj); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
public static string GenerateObj(IEnumerable<Viewport3DTriangleEntity> triangles) |
|
|
|
|
{ |
|
|
|
|
var groups = triangles.GroupBy(t => t.PlaneCode); |
|
|
|
|
|
|
|
|
|
// 存储每个面的顶点和法线 |
|
|
|
|
var faceData = new Dictionary<string, FaceData>(); |
|
|
|
|
List<Vector3> allVertices = new List<Vector3>(); |
|
|
|
|
List<Vector3> allNormals = new List<Vector3>(); |
|
|
|
|
List<string> faceLines = new List<string>(); |
|
|
|
|
|
|
|
|
|
// Step 1: 处理每个面,生成顶点列表并计算法线 |
|
|
|
|
foreach (var group in faceGroups) |
|
|
|
|
{ |
|
|
|
|
// 合并所有顶点并去重 |
|
|
|
|
var vertices = group.Value |
|
|
|
|
.SelectMany(e => new[] { e.Point1, e.Point2, e.Point3 }) |
|
|
|
|
.Distinct() |
|
|
|
|
.ToList(); |
|
|
|
|
foreach (var group in groups) |
|
|
|
|
{ |
|
|
|
|
// Step 1: 组内顶点去重 |
|
|
|
|
var localVertices = new Dictionary<Vector3, int>(); |
|
|
|
|
var groupVertices = new List<Vector3>(); |
|
|
|
|
|
|
|
|
|
// 按凸包算法排序顶点(避免交叉) |
|
|
|
|
var sortedVertices = ConvexHullSort(vertices); |
|
|
|
|
foreach (var triangle in group) |
|
|
|
|
{ |
|
|
|
|
AddVertex(triangle.Point1); |
|
|
|
|
AddVertex(triangle.Point2); |
|
|
|
|
AddVertex(triangle.Point3); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 计算面的法线(基于排序后的顶点) |
|
|
|
|
var normal = CalculateFaceNormal(sortedVertices, ViewportManager.CenterVector); |
|
|
|
|
void AddVertex(Vector3 v) |
|
|
|
|
{ |
|
|
|
|
if (!localVertices.ContainsKey(v)) |
|
|
|
|
{ |
|
|
|
|
localVertices[v] = groupVertices.Count; |
|
|
|
|
groupVertices.Add(v); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 存储面数据 |
|
|
|
|
faceData[group.Key] = new FaceData |
|
|
|
|
{ |
|
|
|
|
Vertices = sortedVertices, |
|
|
|
|
Normal = normal |
|
|
|
|
}; |
|
|
|
|
} |
|
|
|
|
// 映射组内索引到全局索引 |
|
|
|
|
var vertexMap = new Dictionary<int, int>(); |
|
|
|
|
foreach (var v in groupVertices) |
|
|
|
|
{ |
|
|
|
|
allVertices.Add(v); |
|
|
|
|
vertexMap[localVertices[v]] = allVertices.Count; // OBJ索引从1开始 |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// Step 2: 写入 OBJ 文件 |
|
|
|
|
using (var writer = new StreamWriter(outputPath)) |
|
|
|
|
{ |
|
|
|
|
// 写入顶点 |
|
|
|
|
var vertexIndexMap = new Dictionary<Vector3, int>(); |
|
|
|
|
int index = 1; |
|
|
|
|
foreach (var face in faceData.Values) |
|
|
|
|
{ |
|
|
|
|
foreach (var vertex in face.Vertices) |
|
|
|
|
{ |
|
|
|
|
if (!vertexIndexMap.ContainsKey(vertex)) |
|
|
|
|
// Step 2: 处理法线和面 |
|
|
|
|
Vector3 baseNormal = Vector3.Zero; |
|
|
|
|
bool isFirstTriangle = true; |
|
|
|
|
int vnIndex = 0; |
|
|
|
|
|
|
|
|
|
foreach (var triangle in group) |
|
|
|
|
{ |
|
|
|
|
vertexIndexMap[vertex] = index++; |
|
|
|
|
writer.WriteLine($"v {vertex.X} {vertex.Y} {vertex.Z}"); |
|
|
|
|
Vector3 p1 = triangle.Point1; |
|
|
|
|
Vector3 p2 = triangle.Point2; |
|
|
|
|
Vector3 p3 = triangle.Point3; |
|
|
|
|
|
|
|
|
|
int localI1 = localVertices[p1]; |
|
|
|
|
int localI2 = localVertices[p2]; |
|
|
|
|
int localI3 = localVertices[p3]; |
|
|
|
|
|
|
|
|
|
int globalI1 = vertexMap[localI1]; |
|
|
|
|
int globalI2 = vertexMap[localI2]; |
|
|
|
|
int globalI3 = vertexMap[localI3]; |
|
|
|
|
|
|
|
|
|
// 计算法线 |
|
|
|
|
Vector3 edge1 = p2 - p1; |
|
|
|
|
Vector3 edge2 = p3 - p1; |
|
|
|
|
Vector3 normal = Vector3.Cross(edge1, edge2); |
|
|
|
|
normal.Normalize(); |
|
|
|
|
|
|
|
|
|
if (isFirstTriangle) |
|
|
|
|
{ |
|
|
|
|
baseNormal = normal; |
|
|
|
|
isFirstTriangle = false; |
|
|
|
|
allNormals.Add(baseNormal); |
|
|
|
|
vnIndex = allNormals.Count; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 调整法线方向 |
|
|
|
|
float dot = Vector3.Dot(normal, baseNormal); |
|
|
|
|
if (dot < 0) |
|
|
|
|
{ |
|
|
|
|
(globalI2, globalI3) = (globalI3, globalI2); |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
faceLines.Add($"f {globalI1}//{vnIndex} {globalI2}//{vnIndex} {globalI3}//{vnIndex}"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 写入法线(每个面一个法线) |
|
|
|
|
var normalIndexMap = new Dictionary<Vector3, int>(); |
|
|
|
|
index = 1; |
|
|
|
|
foreach (var face in faceData.Values) |
|
|
|
|
{ |
|
|
|
|
if (!normalIndexMap.ContainsKey(face.Normal)) |
|
|
|
|
// 构建OBJ内容 |
|
|
|
|
StringBuilder objBuilder = new StringBuilder(); |
|
|
|
|
|
|
|
|
|
// 顶点 |
|
|
|
|
foreach (var v in allVertices) |
|
|
|
|
{ |
|
|
|
|
normalIndexMap[face.Normal] = index++; |
|
|
|
|
// writer.WriteLine($"vn {face.Normal.X} {face.Normal.Y} {face.Normal.Z}"); |
|
|
|
|
objBuilder.AppendLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
// 写入面 |
|
|
|
|
foreach (var faceEntry in faceData) |
|
|
|
|
{ |
|
|
|
|
var face = faceEntry.Value; |
|
|
|
|
var normalIndex = normalIndexMap[face.Normal]; |
|
|
|
|
|
|
|
|
|
// 正面(顺时针) |
|
|
|
|
writer.Write("f "); |
|
|
|
|
foreach (var vertex in face.Vertices) |
|
|
|
|
// 法线 |
|
|
|
|
foreach (var vn in allNormals) |
|
|
|
|
{ |
|
|
|
|
writer.Write($"{vertexIndexMap[vertex]}//{normalIndex} "); |
|
|
|
|
objBuilder.AppendLine($"vn {vn.X:F6} {vn.Y:F6} {vn.Z:F6}"); |
|
|
|
|
} |
|
|
|
|
writer.WriteLine(); |
|
|
|
|
|
|
|
|
|
// 反面(逆时针) |
|
|
|
|
writer.Write("f "); |
|
|
|
|
foreach (var vertex in face.Vertices.AsEnumerable().Reverse()) |
|
|
|
|
// 面 |
|
|
|
|
foreach (var face in faceLines) |
|
|
|
|
{ |
|
|
|
|
writer.Write($"{vertexIndexMap[vertex]}//{normalIndex} "); |
|
|
|
|
objBuilder.AppendLine(face); |
|
|
|
|
} |
|
|
|
|
writer.WriteLine(); |
|
|
|
|
|
|
|
|
|
return objBuilder.ToString(); |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/// <summary> |
|
|
|
|
/// 凸包排序(Andrew's Monotone Chain 算法) |
|
|
|
|