You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
207 lines
6.6 KiB
207 lines
6.6 KiB
using System.IO; |
|
using System.Text; |
|
using HelixToolkit.SharpDX.Core; |
|
using SharpDX; |
|
using SparkClient.Views.UserControl.ViewportData.Entity; |
|
using SparkClient.Views.UserControl.ViewportData.Enum; |
|
|
|
namespace SparkClient.Views.UserControl.ViewportData.Helper; |
|
|
|
public class ObjExporter |
|
{ |
|
public static void ExportToObj2(List<Viewport3DTriangleEntity> entities, string outputPath) |
|
{ |
|
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); |
|
|
|
List<Vector3> allVertices = new List<Vector3>(); |
|
List<Vector3> allNormals = new List<Vector3>(); |
|
List<string> faceLines = new List<string>(); |
|
|
|
foreach (var group in groups) |
|
{ |
|
// Step 1: 组内顶点去重 |
|
var localVertices = new Dictionary<Vector3, int>(); |
|
var groupVertices = new List<Vector3>(); |
|
|
|
foreach (var triangle in group) |
|
{ |
|
AddVertex(triangle.Point1); |
|
AddVertex(triangle.Point2); |
|
AddVertex(triangle.Point3); |
|
} |
|
|
|
void AddVertex(Vector3 v) |
|
{ |
|
if (!localVertices.ContainsKey(v)) |
|
{ |
|
localVertices[v] = groupVertices.Count; |
|
groupVertices.Add(v); |
|
} |
|
} |
|
|
|
// 映射组内索引到全局索引 |
|
var vertexMap = new Dictionary<int, int>(); |
|
foreach (var v in groupVertices) |
|
{ |
|
allVertices.Add(v); |
|
vertexMap[localVertices[v]] = allVertices.Count; // OBJ索引从1开始 |
|
} |
|
|
|
// Step 2: 处理法线和面 |
|
Vector3 baseNormal = Vector3.Zero; |
|
bool isFirstTriangle = true; |
|
int vnIndex = 0; |
|
|
|
foreach (var triangle in group) |
|
{ |
|
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}"); |
|
} |
|
} |
|
|
|
// 构建OBJ内容 |
|
StringBuilder objBuilder = new StringBuilder(); |
|
|
|
// 顶点 |
|
foreach (var v in allVertices) |
|
{ |
|
objBuilder.AppendLine($"v {v.X:F6} {v.Y:F6} {v.Z:F6}"); |
|
} |
|
|
|
// 法线 |
|
foreach (var vn in allNormals) |
|
{ |
|
objBuilder.AppendLine($"vn {vn.X:F6} {vn.Y:F6} {vn.Z:F6}"); |
|
} |
|
|
|
// 面 |
|
foreach (var face in faceLines) |
|
{ |
|
objBuilder.AppendLine(face); |
|
} |
|
|
|
return objBuilder.ToString(); |
|
} |
|
|
|
/// <summary> |
|
/// 凸包排序(Andrew's Monotone Chain 算法) |
|
/// </summary> |
|
private static List<Vector3> ConvexHullSort(List<Vector3> points) |
|
{ |
|
if (points.Count <= 3) return points; |
|
|
|
// 按 X 坐标排序,若相同则按 Y 排序 |
|
var sorted = points.OrderBy(p => p.X).ThenBy(p => p.Y).ToList(); |
|
|
|
// 构建下凸包和上凸包 |
|
var lower = new List<Vector3>(); |
|
foreach (var p in sorted) |
|
{ |
|
while (lower.Count >= 2 && Cross(lower[lower.Count - 2], lower[lower.Count - 1], p) <= 0) |
|
lower.RemoveAt(lower.Count - 1); |
|
lower.Add(p); |
|
} |
|
|
|
var upper = new List<Vector3>(); |
|
foreach (var p in sorted.AsEnumerable().Reverse()) |
|
{ |
|
while (upper.Count >= 2 && Cross(upper[upper.Count - 2], upper[upper.Count - 1], p) <= 0) |
|
upper.RemoveAt(upper.Count - 1); |
|
upper.Add(p); |
|
} |
|
|
|
// 合并凸包(移除重复点) |
|
lower.RemoveAt(lower.Count - 1); |
|
upper.RemoveAt(upper.Count - 1); |
|
return lower.Concat(upper).ToList(); |
|
} |
|
|
|
/// <summary> |
|
/// 计算叉积 (p2-p0) × (p1-p0) |
|
/// </summary> |
|
private static float Cross(Vector3 p0, Vector3 p1, Vector3 p2) |
|
{ |
|
return (p1.X - p0.X) * (p2.Z - p0.Z) - (p1.Z - p0.Z) * (p2.X - p0.X); |
|
} |
|
/// <summary> |
|
/// 计算面的法线(基于顶点顺序) |
|
/// </summary> |
|
private static Vector3 CalculateFaceNormal(List<Vector3> vertices, Vector3 modelCenter) |
|
{ |
|
// 使用前三个顶点计算初始法线 |
|
var p0 = vertices[0]; |
|
var p1 = vertices[1]; |
|
var p2 = vertices[2]; |
|
var edge1 = p1 - p0; |
|
var edge2 = p2 - p0; |
|
var normal = Vector3.Cross(edge1, edge2).Normalized(); |
|
|
|
// 确保法线朝外(面中心到模型中心的向量与法线方向一致) |
|
var faceCenter = GetCenter(vertices); |
|
var toModelCenter = modelCenter - faceCenter; |
|
if (Vector3.Dot(normal, toModelCenter) > 0) |
|
{ |
|
normal = -normal; |
|
} |
|
|
|
return normal; |
|
} |
|
|
|
/// <summary> |
|
/// 计算顶点列表的中心点 |
|
/// </summary> |
|
private static Vector3 GetCenter(List<Vector3> vertices) |
|
{ |
|
var center = Vector3.Zero; |
|
foreach (var v in vertices) center += v; |
|
return center / vertices.Count; |
|
} |
|
} |
|
|
|
class FaceData |
|
{ |
|
public List<Vector3> Vertices { get; set; } |
|
public Vector3 Normal { get; set; } |
|
} |