Created
April 30, 2026 18:27
-
-
Save Nukoooo/5095ea418e1c58a1d45a40624ff1a37f to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| [StructLayout(LayoutKind.Sequential, Pack = 1)] | |
| public struct RnHalfEdge | |
| { | |
| public byte Next; // Byte 0 | |
| public byte Twin; // Byte 1 | |
| public byte OriginVertex; // Byte 2 | |
| public byte Face; // Byte 3 | |
| } | |
| public void ExtractCleanPerimeterBeams(string modelPath, bool drawTop = false) | |
| { | |
| var kv = _bridge.ModSharp.CreateKeyValues3(KeyValues3Type.Null, KeyValues3SubType.Null); | |
| try | |
| { | |
| if (!kv.LoadFromCompiledFile(modelPath, "GAME", ResourceBlockType.PHYS, out var error)) | |
| { | |
| _logger.LogInformation("Failed to load kv"); | |
| return; | |
| } | |
| if (kv.FindMember("m_parts") is not { } m_parts) | |
| return; | |
| var m_hulls = m_parts.GetArrayElement(0)?.FindMember("m_rnShape")?.FindMember("m_hulls"); | |
| if (m_hulls == null) | |
| return; | |
| // 1. Pass 1: Find Absolute Floor & Ceil Z | |
| var floorZ = float.MaxValue; | |
| var ceilZ = float.MinValue; | |
| var hullCount = m_hulls.GetArrayElementCount(); | |
| for (var i = 0; i < hullCount; i++) | |
| { | |
| var boundsKv = m_hulls.GetArrayElement(i)?.FindMember("m_Hull")?.FindMember("m_Bounds"); | |
| if (boundsKv == null) | |
| continue; | |
| if (boundsKv.FindMember("m_vMinBounds") is { } minBoundsArr) | |
| { | |
| var hullMinZ = minBoundsArr.GetArrayElement(2)?.GetFloat() ?? float.MaxValue; | |
| if (hullMinZ < floorZ) | |
| floorZ = hullMinZ; | |
| } | |
| if (boundsKv.FindMember("m_vMaxBounds") is { } maxBoundsArr) | |
| { | |
| var hullMaxZ = maxBoundsArr.GetArrayElement(2)?.GetFloat() ?? float.MinValue; | |
| if (hullMaxZ > ceilZ) | |
| ceilZ = hullMaxZ; | |
| } | |
| } | |
| Console.WriteLine($"Calculated Absolute Floor Z: {floorZ}"); | |
| if (drawTop) | |
| Console.WriteLine($"Calculated Absolute Ceil Z: {ceilZ}"); | |
| var allUniqueCorners = new HashSet<Vector>(new VectorToleranceComparer()); | |
| var rawEdges = new List<Edge>(); | |
| // 2. Pass 2: Trace topology | |
| for (var i = 0; i < hullCount; i++) | |
| { | |
| if (m_hulls.GetArrayElement(i)?.FindMember("m_Hull") is not { } innerHull) | |
| continue; | |
| if (innerHull.FindMember("m_VertexPositions") is not { } m_VertexPositions | |
| || innerHull.FindMember("m_Edges") is not { } m_Edges | |
| || innerHull.FindMember("m_Faces") is not { } m_Faces) | |
| continue; | |
| var vBytes = m_VertexPositions.GetBinaryBlob(); | |
| var eBytes = m_Edges.GetBinaryBlob(); | |
| var fBytes = m_Faces.GetBinaryBlob(); | |
| if (vBytes.IsEmpty || eBytes.IsEmpty || fBytes.IsEmpty) | |
| continue; | |
| var positions = MemoryMarshal.Cast<byte, Vector>(vBytes); | |
| var edges = MemoryMarshal.Cast<byte, RnHalfEdge>(eBytes); | |
| var faceCorners = new List<Vector>(16); | |
| foreach (var startEdgeIndex in fBytes) | |
| { | |
| var currentEdgeIndex = startEdgeIndex; | |
| var isBottomFace = true; | |
| var isTopFace = true; | |
| faceCorners.Clear(); | |
| do | |
| { | |
| var currentEdge = edges[currentEdgeIndex]; | |
| var cornerPos = positions[currentEdge.OriginVertex]; | |
| faceCorners.Add(cornerPos); | |
| if (Math.Abs(cornerPos.Z - floorZ) > 0.1f) | |
| isBottomFace = false; | |
| if (Math.Abs(cornerPos.Z - ceilZ) > 0.1f) | |
| isTopFace = false; | |
| currentEdgeIndex = currentEdge.Next; | |
| } | |
| while (currentEdgeIndex != startEdgeIndex); | |
| if (isBottomFace || drawTop && isTopFace) | |
| { | |
| for (var c = 0; c < faceCorners.Count; c++) | |
| { | |
| var currentCorner = faceCorners[c]; | |
| var nextCorner = faceCorners[(c + 1) % faceCorners.Count]; | |
| allUniqueCorners.Add(currentCorner); | |
| rawEdges.Add(new Edge(currentCorner, nextCorner)); | |
| } | |
| } | |
| } | |
| } | |
| // 3. Pass 3: THE SPLITTING PASS | |
| var splitEdges = new List<Edge>(); | |
| var pointsOnEdge = new List<Vector>(16); // Re-use buffer | |
| foreach (var edge in rawEdges) | |
| { | |
| var A = edge.V1; | |
| var B = edge.V2; | |
| // Use native DistTo instead of GetDistance | |
| var distAB = A.DistTo(B); | |
| pointsOnEdge.Clear(); | |
| pointsOnEdge.Add(A); | |
| pointsOnEdge.Add(B); | |
| foreach (var corner in allUniqueCorners) | |
| { | |
| // Use native DistToSqr for fast rejection | |
| var distSqrA = A.DistToSqr(corner); | |
| var distSqrB = corner.DistToSqr(B); | |
| if (distSqrA < 0.0001f || distSqrB < 0.0001f) | |
| continue; | |
| var distAC = MathF.Sqrt(distSqrA); | |
| var distCB = MathF.Sqrt(distSqrB); | |
| if (MathF.Abs(distAC + distCB - distAB) < 0.05f) | |
| { | |
| pointsOnEdge.Add(corner); | |
| } | |
| } | |
| // Use native DistToSqr for sorting | |
| pointsOnEdge.Sort((p1, p2) => A.DistToSqr(p1).CompareTo(A.DistToSqr(p2))); | |
| for (var i = 0; i < pointsOnEdge.Count - 1; i++) | |
| { | |
| splitEdges.Add(new Edge(pointsOnEdge[i], pointsOnEdge[i + 1])); | |
| } | |
| } | |
| // 4. Pass 4: THE FREQUENCY PASS | |
| var edgeFrequencies = new Dictionary<Edge, int>(); | |
| foreach (var edge in splitEdges) | |
| { | |
| // TryGetValue is faster than checking TryAdd, and our custom Edge struct handles hashing safely. | |
| edgeFrequencies.TryGetValue(edge, out var count); | |
| edgeFrequencies[edge] = count + 1; | |
| } | |
| // 5. Output and Spawn Entities | |
| Console.WriteLine("\nGenerating Clean Outer Perimeter Beams:"); | |
| var ekv = new Dictionary<string, KeyValuesVariantValueItem> | |
| { | |
| { "rendercolor", "0 255 0" }, | |
| { "boltwidth", "2" }, | |
| }; | |
| if (drawTop) | |
| { | |
| Console.WriteLine("\nGenerating Vertical Pillars:"); | |
| var bottomCorners = new List<Vector>(); | |
| var topCorners = new List<Vector>(); | |
| foreach (var c in allUniqueCorners) | |
| { | |
| if (Math.Abs(c.Z - floorZ) < 0.1f) | |
| bottomCorners.Add(c); | |
| else if (Math.Abs(c.Z - ceilZ) < 0.1f) | |
| topCorners.Add(c); | |
| } | |
| foreach (var bottom in bottomCorners) | |
| { | |
| foreach (var top in topCorners) | |
| { | |
| if (Math.Abs(top.X - bottom.X) < 0.1f && Math.Abs(top.Y - bottom.Y) < 0.1f) | |
| { | |
| Console.WriteLine($"Draw Vertical Pillar from {bottom} to {top}"); | |
| var beam = _bridge.EntityManager.SpawnEntitySync<IBaseModelEntity>("env_beam", ekv); | |
| if (beam is not null) | |
| { | |
| beam.SetAbsOrigin(bottom); | |
| beam.SetNetVar("m_vecEndPos", top); | |
| beam.SetNetVar("m_nBeamType", 2); | |
| } | |
| break; | |
| } | |
| } | |
| } | |
| } | |
| foreach (var kvp in edgeFrequencies) | |
| { | |
| if (kvp.Value == 1) // Outer walls only | |
| { | |
| var edge = kvp.Key; | |
| Console.WriteLine($"Draw Line from {edge.V1} to {edge.V2}"); | |
| var beam = _bridge.EntityManager.SpawnEntitySync<IBaseModelEntity>("env_beam", ekv); | |
| if (beam is null) | |
| continue; | |
| beam.SetAbsOrigin(edge.V1); | |
| beam.SetNetVar("m_vecEndPos", edge.V2); | |
| } | |
| } | |
| } | |
| finally | |
| { | |
| // Guaranteed cleanup regardless of early returns or exceptions | |
| kv.DeleteThis(); | |
| } | |
| } | |
| // Custom Edge struct replaces the expensive string manipulation | |
| public readonly struct Edge : IEquatable<Edge> | |
| { | |
| public readonly Vector V1; | |
| public readonly Vector V2; | |
| public Edge(Vector a, Vector b) | |
| { | |
| // Enforce a strict ordering so that Edge(A,B) equals Edge(B,A) | |
| var isAFirst = a.X < b.X | |
| || Math.Abs(a.X - b.X) < 0.01f && a.Y < b.Y | |
| || Math.Abs(a.X - b.X) < 0.01f && Math.Abs(a.Y - b.Y) < 0.01f && a.Z < b.Z; | |
| if (isAFirst) | |
| { | |
| V1 = a; | |
| V2 = b; | |
| } | |
| else | |
| { | |
| V1 = b; | |
| V2 = a; | |
| } | |
| } | |
| public bool Equals(Edge other) | |
| => Math.Abs(V1.X - other.V1.X) < 0.01f | |
| && Math.Abs(V1.Y - other.V1.Y) < 0.01f | |
| && Math.Abs(V1.Z - other.V1.Z) < 0.01f | |
| && Math.Abs(V2.X - other.V2.X) < 0.01f | |
| && Math.Abs(V2.Y - other.V2.Y) < 0.01f | |
| && Math.Abs(V2.Z - other.V2.Z) < 0.01f; | |
| public override int GetHashCode() | |
| => HashCode.Combine(Math.Round(V1.X), Math.Round(V1.Y), Math.Round(V1.Z), | |
| Math.Round(V2.X), Math.Round(V2.Y), Math.Round(V2.Z)); | |
| public override bool Equals(object obj) | |
| => obj is Edge edge && Equals(edge); | |
| } | |
| public class VectorToleranceComparer : IEqualityComparer<Vector> | |
| { | |
| public bool Equals(Vector v1, Vector v2) | |
| => Math.Abs(v1.X - v2.X) < 0.01f && Math.Abs(v1.Y - v2.Y) < 0.01f && Math.Abs(v1.Z - v2.Z) < 0.01f; | |
| public int GetHashCode(Vector obj) | |
| => HashCode.Combine(Math.Round(obj.X), Math.Round(obj.Y), Math.Round(obj.Z)); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment