using System.Collections.Generic; using UnityEngine; using System.Diagnostics; using SplashEdit.RuntimeCode; public class BSP { private List _objects; private Node root; private const float EPSILON = 1e-6f; private const int MAX_TRIANGLES_PER_LEAF = 256; private const int MAX_TREE_DEPTH = 50; private const int CANDIDATE_PLANE_COUNT = 15; // Statistics private int totalTrianglesProcessed; private int totalSplits; private int treeDepth; private Stopwatch buildTimer; public bool verboseLogging = false; // Store the triangle that was used for the split plane for debugging private Dictionary splitPlaneTriangles = new Dictionary(); private struct Triangle { public Vector3 v0; public Vector3 v1; public Vector3 v2; public Vector3 n0; public Vector3 n1; public Vector3 n2; public Vector2 uv0; public Vector2 uv1; public Vector2 uv2; public Plane plane; public Bounds bounds; public PSXObjectExporter sourceExporter; public int materialIndex; // Store material index instead of submesh index public Triangle(Vector3 a, Vector3 b, Vector3 c, Vector3 na, Vector3 nb, Vector3 nc, Vector2 uva, Vector2 uvb, Vector2 uvc, PSXObjectExporter exporter, int matIndex) { v0 = a; v1 = b; v2 = c; n0 = na; n1 = nb; n2 = nc; uv0 = uva; uv1 = uvb; uv2 = uvc; sourceExporter = exporter; materialIndex = matIndex; // Calculate plane Vector3 edge1 = v1 - v0; Vector3 edge2 = v2 - v0; Vector3 normal = Vector3.Cross(edge1, edge2); if (normal.sqrMagnitude < 1e-4f) { plane = new Plane(Vector3.up, 0); } else { normal.Normalize(); plane = new Plane(normal, v0); } // Calculate bounds bounds = new Bounds(v0, Vector3.zero); bounds.Encapsulate(v1); bounds.Encapsulate(v2); } public void Transform(Matrix4x4 matrix) { v0 = matrix.MultiplyPoint3x4(v0); v1 = matrix.MultiplyPoint3x4(v1); v2 = matrix.MultiplyPoint3x4(v2); // Transform normals (using inverse transpose for correct scaling) Matrix4x4 invTranspose = matrix.inverse.transpose; n0 = invTranspose.MultiplyVector(n0).normalized; n1 = invTranspose.MultiplyVector(n1).normalized; n2 = invTranspose.MultiplyVector(n2).normalized; // Recalculate plane and bounds after transformation Vector3 edge1 = v1 - v0; Vector3 edge2 = v2 - v0; Vector3 normal = Vector3.Cross(edge1, edge2); if (normal.sqrMagnitude < 1e-4f) { plane = new Plane(Vector3.up, 0); } else { normal.Normalize(); plane = new Plane(normal, v0); } bounds = new Bounds(v0, Vector3.zero); bounds.Encapsulate(v1); bounds.Encapsulate(v2); } } private class Node { public Plane plane; public Node front; public Node back; public List triangles; public bool isLeaf = false; public Bounds bounds; public int depth; public int triangleSourceIndex = -1; } public BSP(List objects) { _objects = objects; buildTimer = new Stopwatch(); } public void Build() { buildTimer.Start(); List triangles = ExtractTrianglesFromMeshes(); totalTrianglesProcessed = triangles.Count; if (verboseLogging) UnityEngine.Debug.Log($"Starting BSP build with {totalTrianglesProcessed} triangles"); if (triangles.Count == 0) { root = null; return; } // Calculate overall bounds Bounds overallBounds = CalculateBounds(triangles); // Build tree recursively with depth tracking root = BuildNode(triangles, overallBounds, 0); // Create modified meshes for all exporters CreateModifiedMeshes(); buildTimer.Stop(); if (verboseLogging) { UnityEngine.Debug.Log($"BSP build completed in {buildTimer.Elapsed.TotalMilliseconds}ms"); UnityEngine.Debug.Log($"Total splits: {totalSplits}, Max depth: {treeDepth}"); } } private List ExtractTrianglesFromMeshes() { List triangles = new List(); foreach (var meshObj in _objects) { if (!meshObj.IsActive) continue; MeshFilter mf = meshObj.GetComponent(); Renderer renderer = meshObj.GetComponent(); if (mf == null || mf.sharedMesh == null || renderer == null) continue; Mesh mesh = mf.sharedMesh; Vector3[] vertices = mesh.vertices; Vector3[] normals = mesh.normals.Length > 0 ? mesh.normals : new Vector3[vertices.Length]; Vector2[] uvs = mesh.uv.Length > 0 ? mesh.uv : new Vector2[vertices.Length]; Matrix4x4 matrix = meshObj.transform.localToWorldMatrix; // Handle case where normals are missing if (mesh.normals.Length == 0) { for (int i = 0; i < normals.Length; i++) { normals[i] = Vector3.up; } } // Handle case where UVs are missing if (mesh.uv.Length == 0) { for (int i = 0; i < uvs.Length; i++) { uvs[i] = Vector2.zero; } } // Process each submesh and track material index for (int submesh = 0; submesh < mesh.subMeshCount; submesh++) { int materialIndex = Mathf.Min(submesh, renderer.sharedMaterials.Length - 1); int[] indices = mesh.GetTriangles(submesh); for (int i = 0; i < indices.Length; i += 3) { int idx0 = indices[i]; int idx1 = indices[i + 1]; int idx2 = indices[i + 2]; Vector3 v0 = vertices[idx0]; Vector3 v1 = vertices[idx1]; Vector3 v2 = vertices[idx2]; // Skip degenerate triangles if (Vector3.Cross(v1 - v0, v2 - v0).sqrMagnitude < 1e-4f) continue; Vector3 n0 = normals[idx0]; Vector3 n1 = normals[idx1]; Vector3 n2 = normals[idx2]; Vector2 uv0 = uvs[idx0]; Vector2 uv1 = uvs[idx1]; Vector2 uv2 = uvs[idx2]; Triangle tri = new Triangle(v0, v1, v2, n0, n1, n2, uv0, uv1, uv2, meshObj, materialIndex); tri.Transform(matrix); triangles.Add(tri); } } } return triangles; } private Node BuildNode(List triangles, Bounds bounds, int depth) { if (triangles == null || triangles.Count == 0) return null; Node node = new Node { triangles = new List(), bounds = bounds, depth = depth }; treeDepth = Mathf.Max(treeDepth, depth); // Create leaf node if conditions are met if (triangles.Count <= MAX_TRIANGLES_PER_LEAF || depth >= MAX_TREE_DEPTH) { node.isLeaf = true; node.triangles = triangles; if (verboseLogging && depth >= MAX_TREE_DEPTH) UnityEngine.Debug.LogWarning($"Max tree depth reached at depth {depth} with {triangles.Count} triangles"); return node; } // Select the best splitting plane using multiple strategies Triangle? splitTriangle = null; if (!SelectBestSplittingPlane(triangles, bounds, out node.plane, out splitTriangle)) { // Fallback: create leaf if no good split found node.isLeaf = true; node.triangles = triangles; if (verboseLogging) UnityEngine.Debug.Log($"Created leaf node with {triangles.Count} triangles (no good split found)"); return node; } // Store the triangle that provided the split plane for debugging if (splitTriangle.HasValue) { splitPlaneTriangles[node] = splitTriangle.Value; } List frontList = new List(); List backList = new List(); List coplanarList = new List(); // Classify all triangles foreach (var tri in triangles) { ClassifyTriangle(tri, node.plane, coplanarList, frontList, backList); } // Handle cases where splitting doesn't provide benefit if (frontList.Count == 0 || backList.Count == 0) { // If split doesn't separate geometry, create a leaf node.isLeaf = true; node.triangles = triangles; if (verboseLogging) UnityEngine.Debug.Log($"Created leaf node with {triangles.Count} triangles (ineffective split)"); return node; } // Distribute coplanar triangles to the side with fewer triangles if (coplanarList.Count > 0) { if (frontList.Count <= backList.Count) { frontList.AddRange(coplanarList); } else { backList.AddRange(coplanarList); } } if (verboseLogging) UnityEngine.Debug.Log($"Node at depth {depth}: {triangles.Count} triangles -> {frontList.Count} front, {backList.Count} back"); // Calculate bounds for children Bounds frontBounds = CalculateBounds(frontList); Bounds backBounds = CalculateBounds(backList); // Recursively build child nodes node.front = BuildNode(frontList, frontBounds, depth + 1); node.back = BuildNode(backList, backBounds, depth + 1); return node; } private bool SelectBestSplittingPlane(List triangles, Bounds bounds, out Plane bestPlane, out Triangle? splitTriangle) { bestPlane = new Plane(); splitTriangle = null; int bestScore = int.MaxValue; bool foundValidPlane = false; // Strategy 1: Try planes from triangle centroids int candidatesToTry = Mathf.Min(CANDIDATE_PLANE_COUNT, triangles.Count); for (int i = 0; i < candidatesToTry; i++) { Triangle tri = triangles[i]; Plane candidate = tri.plane; int score = EvaluateSplitPlane(triangles, candidate); if (score < bestScore && score >= 0) { bestScore = score; bestPlane = candidate; splitTriangle = tri; foundValidPlane = true; } } // Strategy 2: Try axis-aligned planes through bounds center if (!foundValidPlane || bestScore > triangles.Count * 3) { Vector3[] axes = { Vector3.right, Vector3.up, Vector3.forward }; for (int i = 0; i < 3; i++) { Plane candidate = new Plane(axes[i], bounds.center); int score = EvaluateSplitPlane(triangles, candidate); if (score < bestScore && score >= 0) { bestScore = score; bestPlane = candidate; splitTriangle = null; foundValidPlane = true; } } } // Strategy 3: Try planes based on bounds extents if (!foundValidPlane) { Vector3 extents = bounds.extents; if (extents.x >= extents.y && extents.x >= extents.z) bestPlane = new Plane(Vector3.right, bounds.center); else if (extents.y >= extents.x && extents.y >= extents.z) bestPlane = new Plane(Vector3.up, bounds.center); else bestPlane = new Plane(Vector3.forward, bounds.center); splitTriangle = null; foundValidPlane = true; } return foundValidPlane; } private int EvaluateSplitPlane(List triangles, Plane plane) { int frontCount = 0; int backCount = 0; int splitCount = 0; int coplanarCount = 0; foreach (var tri in triangles) { float d0 = plane.GetDistanceToPoint(tri.v0); float d1 = plane.GetDistanceToPoint(tri.v1); float d2 = plane.GetDistanceToPoint(tri.v2); // Check for NaN/infinity if (float.IsNaN(d0) || float.IsNaN(d1) || float.IsNaN(d2) || float.IsInfinity(d0) || float.IsInfinity(d1) || float.IsInfinity(d2)) { return int.MaxValue; } bool front = d0 > EPSILON || d1 > EPSILON || d2 > EPSILON; bool back = d0 < -EPSILON || d1 < -EPSILON || d2 < -EPSILON; if (front && back) splitCount++; else if (front) frontCount++; else if (back) backCount++; else coplanarCount++; } // Reject planes that would cause too many splits or imbalanced trees if (splitCount > triangles.Count / 2) return int.MaxValue; // Score based on balance and split count return Mathf.Abs(frontCount - backCount) + splitCount * 2; } private void ClassifyTriangle(Triangle tri, Plane plane, List coplanar, List front, List back) { float d0 = plane.GetDistanceToPoint(tri.v0); float d1 = plane.GetDistanceToPoint(tri.v1); float d2 = plane.GetDistanceToPoint(tri.v2); // Check for numerical issues if (float.IsNaN(d0) || float.IsNaN(d1) || float.IsNaN(d2) || float.IsInfinity(d0) || float.IsInfinity(d1) || float.IsInfinity(d2)) { coplanar.Add(tri); return; } bool front0 = d0 > EPSILON; bool front1 = d1 > EPSILON; bool front2 = d2 > EPSILON; bool back0 = d0 < -EPSILON; bool back1 = d1 < -EPSILON; bool back2 = d2 < -EPSILON; int fCount = (front0 ? 1 : 0) + (front1 ? 1 : 0) + (front2 ? 1 : 0); int bCount = (back0 ? 1 : 0) + (back1 ? 1 : 0) + (back2 ? 1 : 0); if (fCount == 3) { front.Add(tri); } else if (bCount == 3) { back.Add(tri); } else if (fCount == 0 && bCount == 0) { coplanar.Add(tri); } else { totalSplits++; SplitTriangle(tri, plane, front, back); } } private void SplitTriangle(Triangle tri, Plane plane, List front, List back) { // Get distances float d0 = plane.GetDistanceToPoint(tri.v0); float d1 = plane.GetDistanceToPoint(tri.v1); float d2 = plane.GetDistanceToPoint(tri.v2); // Classify points bool[] frontSide = { d0 > EPSILON, d1 > EPSILON, d2 > EPSILON }; bool[] backSide = { d0 < -EPSILON, d1 < -EPSILON, d2 < -EPSILON }; // Count how many points are on each side int frontCount = (frontSide[0] ? 1 : 0) + (frontSide[1] ? 1 : 0) + (frontSide[2] ? 1 : 0); int backCount = (backSide[0] ? 1 : 0) + (backSide[1] ? 1 : 0) + (backSide[2] ? 1 : 0); // 2 points on one side, 1 on the other if (frontCount == 2 && backCount == 1) { int loneIndex = backSide[0] ? 0 : (backSide[1] ? 1 : 2); SplitTriangle2To1(tri, plane, loneIndex, true, front, back); } else if (backCount == 2 && frontCount == 1) { int loneIndex = frontSide[0] ? 0 : (frontSide[1] ? 1 : 2); SplitTriangle2To1(tri, plane, loneIndex, false, front, back); } else { // Complex case - add to both sides (should be rare) front.Add(tri); back.Add(tri); } } private void SplitTriangle2To1(Triangle tri, Plane plane, int loneIndex, bool loneIsBack, List front, List back) { Vector3[] v = { tri.v0, tri.v1, tri.v2 }; Vector3[] n = { tri.n0, tri.n1, tri.n2 }; Vector2[] uv = { tri.uv0, tri.uv1, tri.uv2 }; Vector3 loneVertex = v[loneIndex]; Vector3 loneNormal = n[loneIndex]; Vector2 loneUV = uv[loneIndex]; Vector3 v1 = v[(loneIndex + 1) % 3]; Vector3 v2 = v[(loneIndex + 2) % 3]; Vector3 n1 = n[(loneIndex + 1) % 3]; Vector3 n2 = n[(loneIndex + 2) % 3]; Vector2 uv1 = uv[(loneIndex + 1) % 3]; Vector2 uv2 = uv[(loneIndex + 2) % 3]; Vector3 i1 = PlaneIntersection(plane, loneVertex, v1); float t1 = CalculateInterpolationFactor(plane, loneVertex, v1); Vector3 n_i1 = Vector3.Lerp(loneNormal, n1, t1).normalized; Vector2 uv_i1 = Vector2.Lerp(loneUV, uv1, t1); Vector3 i2 = PlaneIntersection(plane, loneVertex, v2); float t2 = CalculateInterpolationFactor(plane, loneVertex, v2); Vector3 n_i2 = Vector3.Lerp(loneNormal, n2, t2).normalized; Vector2 uv_i2 = Vector2.Lerp(loneUV, uv2, t2); // Desired normal: prefer triangle's plane normal, fallback to geometric normal Vector3 desired = tri.plane.normal; if (desired.sqrMagnitude < 1e-4f) desired = Vector3.Cross(tri.v1 - tri.v0, tri.v2 - tri.v0).normalized; if (desired.sqrMagnitude < 1e-4f) desired = Vector3.up; // Helper: decide and swap b/c if necessary, then add triangle void AddTriClockwise(List list, Vector3 a, Vector3 b, Vector3 c, Vector3 na, Vector3 nb, Vector3 nc, Vector2 ua, Vector2 ub, Vector2 uc) { Vector3 cross = Vector3.Cross(b - a, c - a); if (cross.z > 0f) // <-- assumes you're working in PS1 screen space (z forward) { // swap b <-> c var tmpV = b; b = c; c = tmpV; var tmpN = nb; nb = nc; nc = tmpN; var tmpUv = ub; ub = uc; uc = tmpUv; } list.Add(new Triangle(a, b, c, na, nb, nc, ua, ub, uc, tri.sourceExporter, tri.materialIndex)); } if (loneIsBack) { // back: (lone, i1, i2) AddTriClockwise(back, loneVertex, i1, i2, loneNormal, n_i1, n_i2, loneUV, uv_i1, uv_i2); // front: (v1, i1, i2) and (v1, i2, v2) AddTriClockwise(front, v1, i1, i2, n1, n_i1, n_i2, uv1, uv_i1, uv_i2); AddTriClockwise(front, v1, i2, v2, n1, n_i2, n2, uv1, uv_i2, uv2); } else { // front: (lone, i1, i2) AddTriClockwise(front, loneVertex, i1, i2, loneNormal, n_i1, n_i2, loneUV, uv_i1, uv_i2); // back: (v1, i1, i2) and (v1, i2, v2) AddTriClockwise(back, v1, i1, i2, n1, n_i1, n_i2, uv1, uv_i1, uv_i2); AddTriClockwise(back, v1, i2, v2, n1, n_i2, n2, uv1, uv_i2, uv2); } } private Vector3 PlaneIntersection(Plane plane, Vector3 a, Vector3 b) { Vector3 ba = b - a; float denominator = Vector3.Dot(plane.normal, ba); // Check for parallel line (shouldn't happen in our case) if (Mathf.Abs(denominator) < 1e-4f) return a; float t = (-plane.distance - Vector3.Dot(plane.normal, a)) / denominator; return a + ba * Mathf.Clamp01(t); } private float CalculateInterpolationFactor(Plane plane, Vector3 a, Vector3 b) { Vector3 ba = b - a; float denominator = Vector3.Dot(plane.normal, ba); if (Mathf.Abs(denominator) < 1e-4f) return 0.5f; float t = (-plane.distance - Vector3.Dot(plane.normal, a)) / denominator; return Mathf.Clamp01(t); } private Bounds CalculateBounds(List triangles) { if (triangles == null || triangles.Count == 0) return new Bounds(); Bounds bounds = triangles[0].bounds; for (int i = 1; i < triangles.Count; i++) { bounds.Encapsulate(triangles[i].bounds); } return bounds; } // Add a method to create modified meshes after BSP construction // Add a method to create modified meshes after BSP construction private void CreateModifiedMeshes() { if (root == null) return; // Collect all triangles from the BSP tree List allTriangles = new List(); CollectTrianglesFromNode(root, allTriangles); // Group triangles by their source exporter and material index Dictionary>> exporterTriangles = new Dictionary>>(); foreach (var tri in allTriangles) { if (!exporterTriangles.ContainsKey(tri.sourceExporter)) { exporterTriangles[tri.sourceExporter] = new Dictionary>(); } var materialDict = exporterTriangles[tri.sourceExporter]; if (!materialDict.ContainsKey(tri.materialIndex)) { materialDict[tri.materialIndex] = new List(); } materialDict[tri.materialIndex].Add(tri); } // Create modified meshes for each exporter foreach (var kvp in exporterTriangles) { PSXObjectExporter exporter = kvp.Key; Dictionary> materialTriangles = kvp.Value; Mesh originalMesh = exporter.GetComponent().sharedMesh; Renderer renderer = exporter.GetComponent(); Mesh modifiedMesh = new Mesh(); modifiedMesh.name = originalMesh.name + "_BSP"; List vertices = new List(); List normals = new List(); List uvs = new List(); List tangents = new List(); List colors = new List(); // Create a list for each material's triangles List> materialIndices = new List>(); for (int i = 0; i < renderer.sharedMaterials.Length; i++) { materialIndices.Add(new List()); } // Get the inverse transform to convert from world space back to object space Matrix4x4 worldToLocal = exporter.transform.worldToLocalMatrix; // Process each material foreach (var materialKvp in materialTriangles) { int materialIndex = materialKvp.Key; List triangles = materialKvp.Value; // Add vertices, normals, and uvs for this material for (int i = 0; i < triangles.Count; i++) { Triangle tri = triangles[i]; // Transform vertices from world space back to object space Vector3 v0 = worldToLocal.MultiplyPoint3x4(tri.v0); Vector3 v1 = worldToLocal.MultiplyPoint3x4(tri.v1); Vector3 v2 = worldToLocal.MultiplyPoint3x4(tri.v2); int vertexIndex = vertices.Count; vertices.Add(v0); vertices.Add(v1); vertices.Add(v2); // Transform normals from world space back to object space Vector3 n0 = worldToLocal.MultiplyVector(tri.n0).normalized; Vector3 n1 = worldToLocal.MultiplyVector(tri.n1).normalized; Vector3 n2 = worldToLocal.MultiplyVector(tri.n2).normalized; normals.Add(n0); normals.Add(n1); normals.Add(n2); uvs.Add(tri.uv0); uvs.Add(tri.uv1); uvs.Add(tri.uv2); // Add default tangents and colors (will be recalculated later) tangents.Add(new Vector4(1, 0, 0, 1)); tangents.Add(new Vector4(1, 0, 0, 1)); tangents.Add(new Vector4(1, 0, 0, 1)); colors.Add(Color.white); colors.Add(Color.white); colors.Add(Color.white); // Add indices for this material materialIndices[materialIndex].Add(vertexIndex); materialIndices[materialIndex].Add(vertexIndex + 1); materialIndices[materialIndex].Add(vertexIndex + 2); } } // Assign data to the mesh modifiedMesh.vertices = vertices.ToArray(); modifiedMesh.normals = normals.ToArray(); modifiedMesh.uv = uvs.ToArray(); modifiedMesh.tangents = tangents.ToArray(); modifiedMesh.colors = colors.ToArray(); // Set up submeshes based on materials modifiedMesh.subMeshCount = materialIndices.Count; for (int i = 0; i < materialIndices.Count; i++) { modifiedMesh.SetTriangles(materialIndices[i].ToArray(), i); } // Recalculate important mesh properties modifiedMesh.RecalculateBounds(); modifiedMesh.RecalculateTangents(); // Assign the modified mesh to the exporter exporter.ModifiedMesh = modifiedMesh; } } // Helper method to collect all triangles from the BSP tree private void CollectTrianglesFromNode(Node node, List triangles) { if (node == null) return; if (node.isLeaf) { triangles.AddRange(node.triangles); } else { CollectTrianglesFromNode(node.front, triangles); CollectTrianglesFromNode(node.back, triangles); } } public void DrawGizmos(int maxDepth) { if (root == null) return; DrawNodeGizmos(root, 0, maxDepth); } private void DrawNodeGizmos(Node node, int depth, int maxDepth) { if (node == null) return; if (depth > maxDepth) return; Color nodeColor = Color.HSVToRGB((depth * 0.1f) % 1f, 0.8f, 0.8f); Gizmos.color = nodeColor; if (node.isLeaf) { foreach (var tri in node.triangles) { DrawTriangleGizmo(tri); } } else { DrawPlaneGizmo(node.plane, node.bounds); // Draw the triangle that was used for the split plane if available if (splitPlaneTriangles.ContainsKey(node)) { Gizmos.color = Color.magenta; DrawTriangleGizmo(splitPlaneTriangles[node]); Gizmos.color = nodeColor; } DrawNodeGizmos(node.front, depth + 1, maxDepth); DrawNodeGizmos(node.back, depth + 1, maxDepth); } } private void DrawTriangleGizmo(Triangle tri) { Gizmos.DrawLine(tri.v0, tri.v1); Gizmos.DrawLine(tri.v1, tri.v2); Gizmos.DrawLine(tri.v2, tri.v0); } private void DrawPlaneGizmo(Plane plane, Bounds bounds) { Vector3 center = bounds.center; Vector3 normal = plane.normal; Vector3 tangent = Vector3.Cross(normal, Vector3.up); if (tangent.magnitude < 0.1f) tangent = Vector3.Cross(normal, Vector3.right); tangent = tangent.normalized; Vector3 bitangent = Vector3.Cross(normal, tangent).normalized; float size = Mathf.Max(bounds.size.x, bounds.size.y, bounds.size.z) * 0.5f; tangent *= size; bitangent *= size; Vector3 p0 = center - tangent - bitangent; Vector3 p1 = center + tangent - bitangent; Vector3 p2 = center + tangent + bitangent; Vector3 p3 = center - tangent + bitangent; Gizmos.DrawLine(p0, p1); Gizmos.DrawLine(p1, p2); Gizmos.DrawLine(p2, p3); Gizmos.DrawLine(p3, p0); Gizmos.color = Color.red; Gizmos.DrawLine(center, center + normal * size * 0.5f); } }