Skip to content

Instantly share code, notes, and snippets.

@Nukoooo
Created April 30, 2026 18:27
Show Gist options
  • Select an option

  • Save Nukoooo/5095ea418e1c58a1d45a40624ff1a37f to your computer and use it in GitHub Desktop.

Select an option

Save Nukoooo/5095ea418e1c58a1d45a40624ff1a37f to your computer and use it in GitHub Desktop.
[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