Files
LightlessClient/LightlessSync/ThirdParty/Nanomesh/Mesh/ConnectedMesh/ConnectedMesh.cs
defnotken 72a62b7449
All checks were successful
Tag and Release Lightless / tag-and-release (push) Successful in 2m9s
2.1.0 (#123)
# Patchnotes 2.1.0
The changes in this update are more than just "patches". With a new UI, a new feature, and a bunch of bug fixes, improvements and a new member on the dev team, we thought this was more of a minor update.

We would like to introduce @tsubasahane of MareCN to the team! We’re happy to work with them to bring Lightless and its features to the CN client as well as having another talented dev bring features and ideas to us. Speaking of which:

# Location Sharing (Big shout out to @tsubasahane for bringing this feature)

- Are you TIRED of scrambling to find the address of the venue you're in to share with your friends? We are introducing Location Sharing! An optional feature where you can share your location with direct pairs temporarily [30 minutes, 1 hour, 3 hours] minutes or until you turn it off for them. That's up to you! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To share your location with a pair, click the three dots beside the pair and choose a duration to share with them. [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)
- To view the location of someone who's shared with you, simply hover over the globe icon! [#125](<#125>)  [#49](<Lightless-Sync/LightlessServer#49>)

[1]

# Model Optimization (Mesh Decimating)
 - This new option can automatically “simplify” incoming character meshes to help performance by reducing triangle counts. You choose how strong the reduction is (default/recommended is 80%). [#131](<#131>)
 - Decimation only kicks in when a mesh is above a certain triangle threshold, and only for the items that qualify for it and you selected for. [#131](<#131>)
 - Hair meshes is always excluded, since simplifying hair meshes is very prone to breaking.
 - You can find everything under Settings → Performance → Model Optimization. [#131](<#131>)
+ ** IF YOU HAVE USED DECIMATION IN TESTING, PLEASE CLEAR YOUR CACHE  **

[2]

# Animation (PAP) Validation (Safer animations)
 - Lightless now checks your currently animations to see if they work with your local skeleton/bone mod. If an animation matches, it’s included in what gets sent to other players. If it doesn’t, Lightless will skip it and write a warning to your log showing how many were skipped due to skeleton changes. Its defaulted to Unsafe (off). turn it on if you experience crashes from others users. [#131](<#131>)
 - Lightless also does the same kind of check for incoming animation files, to make sure they match the body/skeleton they were sent with. [#131](<#131>)
 - Because these checks can sometimes be a little picky, you can adjust how strict they are in Settings -> General -> Animation & Bones to reduce false positives. [#131](<#131>)

# UI Changes (Thanks to @kyuwu for UI Changes)
- The top part of the main screen has gotten a makeover. You can adjust the colors of the gradiant in the Color settings of Lightless. [#127](<#127>)

[3]

- Settings have gotten some changes as well to make this change more universal, and will use the same color settings. [#127](<#127>)
- The particle effects of the gradient are toggleable in 'Settings -> UI -> Behavior' [#127](<#127>)
- Instead of showing download/upload on bottom of Main UI, it will show VRAM usage and triangles with their optimization options next to it [#138](<#138>)

# LightFinder / ShellFinder
- UI Changes that follow our new design follow the color codes for the Gradient top as the main screen does.  [#127](<#127>)

[4]

Co-authored-by: defnotken <itsdefnotken@gmail.com>
Co-authored-by: azyges <aaaaaa@aaa.aaa>
Co-authored-by: cake <admin@cakeandbanana.nl>
Co-authored-by: Tsubasa <tsubasa@noreply.git.lightless-sync.org>
Co-authored-by: choco <choco@patat.nl>
Co-authored-by: celine <aaa@aaa.aaa>
Co-authored-by: celine <celine@noreply.git.lightless-sync.org>
Co-authored-by: Tsubasahane <wozaiha@gmail.com>
Co-authored-by: cake <cake@noreply.git.lightless-sync.org>
Reviewed-on: #123
2026-01-20 19:43:00 +00:00

707 lines
22 KiB
C#

using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Nanomesh
{
// Let's say F = 2V
// Halfedge mesh is V * sizeof(vertex) + 3F * sizeof(Halfedge) + F * sizeof(Face) = 16 * 0.5F + 3F * 20 + 4F = 72F
// Connected mesh is V * sizeof(Vector3) + 3F * sizeof(Node) + F * sizeof(Face) = 12 * 0.5F + 3F * 12 + 12F = 54F (without attributes)
// Connected mesh no face is V * sizeof(Vector3) + 3F * sizeof(Node) = 12 * 0.5F + 3F * 12 = 42F (without attributes)
public partial class ConnectedMesh
{
// Todo : make this private (can only be modified from the inside)
public Vector3[] positions;
public MetaAttributeList attributes;
public Node[] nodes;
public Group[] groups;
public AttributeDefinition[] attributeDefinitions;
public int[] PositionToNode => _positionToNode ?? (_positionToNode = GetPositionToNode());
private int[] _positionToNode;
internal int _faceCount;
public int FaceCount => _faceCount;
public bool AreNodesSiblings(int nodeIndexA, int nodeIndexB)
{
return nodes[nodeIndexA].position == nodes[nodeIndexB].position;
}
public int[] GetPositionToNode()
{
int[] positionToNode = new int[positions.Length];
for (int i = 0; i < positions.Length; i++)
{
positionToNode[i] = -1;
}
for (int i = 0; i < nodes.Length; i++)
{
if (!nodes[i].IsRemoved)
{
positionToNode[nodes[i].position] = i;
}
}
return positionToNode;
}
public int GetEdgeCount(int nodeIndex)
{
return GetRelativesCount(nodeIndex) + 1;
}
public int GetRelativesCount(int nodeIndex)
{
int k = 0;
int relative = nodeIndex;
while ((relative = nodes[relative].relative) != nodeIndex)
{
k++;
}
return k;
}
public int GetSiblingsCount(int nodeIndex)
{
int k = 0;
int sibling = nodeIndex;
while ((sibling = nodes[sibling].sibling) != nodeIndex)
{
k++;
}
return k;
}
public int ReconnectSiblings(int nodeIndex)
{
int sibling = nodeIndex;
int lastValid = -1;
int firstValid = -1;
int position = -1;
do
{
if (nodes[sibling].IsRemoved)
{
continue;
}
if (firstValid == -1)
{
firstValid = sibling;
position = nodes[sibling].position;
}
if (lastValid != -1)
{
nodes[lastValid].sibling = sibling;
nodes[lastValid].position = position;
}
lastValid = sibling;
}
while ((sibling = nodes[sibling].sibling) != nodeIndex);
if (lastValid == -1)
{
return -1; // All siblings were removed
}
// Close the loop
nodes[lastValid].sibling = firstValid;
nodes[lastValid].position = position;
return firstValid;
}
public int ReconnectSiblings(int nodeIndexA, int nodeIndexB, int position)
{
int sibling = nodeIndexA;
int lastValid = -1;
int firstValid = -1;
do
{
if (nodes[sibling].IsRemoved)
{
continue;
}
if (firstValid == -1)
{
firstValid = sibling;
//position = nodes[sibling].position;
}
if (lastValid != -1)
{
nodes[lastValid].sibling = sibling;
nodes[lastValid].position = position;
}
lastValid = sibling;
}
while ((sibling = nodes[sibling].sibling) != nodeIndexA);
sibling = nodeIndexB;
do
{
if (nodes[sibling].IsRemoved)
{
continue;
}
if (firstValid == -1)
{
firstValid = sibling;
//position = nodes[sibling].position;
}
if (lastValid != -1)
{
nodes[lastValid].sibling = sibling;
nodes[lastValid].position = position;
}
lastValid = sibling;
}
while ((sibling = nodes[sibling].sibling) != nodeIndexB);
if (lastValid == -1)
{
return -1; // All siblings were removed
}
// Close the loop
nodes[lastValid].sibling = firstValid;
nodes[lastValid].position = position;
return firstValid;
}
public int CollapseEdge(int nodeIndexA, int nodeIndexB)
{
int posA = nodes[nodeIndexA].position;
int posB = nodes[nodeIndexB].position;
Debug.Assert(posA != posB, "A and B must have different positions");
Debug.Assert(!nodes[nodeIndexA].IsRemoved);
Debug.Assert(!nodes[nodeIndexB].IsRemoved);
Debug.Assert(CheckRelatives(nodeIndexA), "A's relatives must be valid");
Debug.Assert(CheckRelatives(nodeIndexB), "B's relatives must be valid");
Debug.Assert(CheckSiblings(nodeIndexA), "A's siblings must be valid");
Debug.Assert(CheckSiblings(nodeIndexB), "B's siblings must be valid");
int siblingOfA = nodeIndexA;
do // Iterates over faces around A
{
bool isFaceTouched = false;
int faceEdgeCount = 0;
int nodeIndexC = -1;
int relativeOfA = siblingOfA;
do // Circulate in face
{
int posC = nodes[relativeOfA].position;
if (posC == posB)
{
isFaceTouched = true;
}
else if (posC != posA)
{
nodeIndexC = relativeOfA;
}
faceEdgeCount++;
} while ((relativeOfA = nodes[relativeOfA].relative) != siblingOfA);
if (faceEdgeCount != 3)
throw new NotImplementedException();
if (isFaceTouched && faceEdgeCount == 3)
{
// Remove face : Mark nodes as removed an reconnect siblings around C
int posC = nodes[nodeIndexC].position;
relativeOfA = siblingOfA;
do
{
nodes[relativeOfA].MarkRemoved();
} while ((relativeOfA = nodes[relativeOfA].relative) != siblingOfA);
int validNodeAtC = ReconnectSiblings(nodeIndexC);
if (_positionToNode != null)
{
_positionToNode[posC] = validNodeAtC;
}
_faceCount--;
}
} while ((siblingOfA = nodes[siblingOfA].sibling) != nodeIndexA);
int validNodeAtA = ReconnectSiblings(nodeIndexA, nodeIndexB, posA);
if (_positionToNode != null)
{
_positionToNode[posA] = validNodeAtA;
_positionToNode[posB] = -1;
}
return validNodeAtA;
}
public double GetEdgeTopo(int nodeIndexA, int nodeIndexB)
{
if ((uint)nodeIndexA >= (uint)nodes.Length || (uint)nodeIndexB >= (uint)nodes.Length)
{
return EdgeBorderPenalty;
}
if (nodes[nodeIndexA].IsRemoved || nodes[nodeIndexB].IsRemoved)
{
return EdgeBorderPenalty;
}
int posB = nodes[nodeIndexB].position;
int facesAttached = 0;
int attrAtA = -1;
int attrAtB = -1;
double edgeWeight = 0;
int siblingOfA = nodeIndexA;
do
{
int relativeOfA = siblingOfA;
while ((relativeOfA = nodes[relativeOfA].relative) != siblingOfA)
{
int posC = nodes[relativeOfA].position;
if (posC == posB)
{
facesAttached++;
if (attributes != null)
{
for (int i = 0; i < attributes.CountPerAttribute; i++)
{
if (attrAtB != -1 && !attributes.Equals(attrAtB, nodes[relativeOfA].attribute, i))
{
edgeWeight += attributeDefinitions[i].weight;
}
if (attrAtA != -1 && !attributes.Equals(attrAtA, nodes[siblingOfA].attribute, i))
{
edgeWeight += attributeDefinitions[i].weight;
}
}
}
attrAtB = nodes[relativeOfA].attribute;
attrAtA = nodes[siblingOfA].attribute;
}
}
} while ((siblingOfA = nodes[siblingOfA].sibling) != nodeIndexA);
if (facesAttached != 2) // Border or non-manifold edge
{
edgeWeight += EdgeBorderPenalty;
}
return edgeWeight;
}
internal static double EdgeBorderPenalty = 1027.007;
// TODO : Make it work with any polygon (other than triangle)
public Vector3 GetFaceNormal(int nodeIndex)
{
int posA = nodes[nodeIndex].position;
int posB = nodes[nodes[nodeIndex].relative].position;
int posC = nodes[nodes[nodes[nodeIndex].relative].relative].position;
Vector3 normal = Vector3.Cross(
positions[posB] - positions[posA],
positions[posC] - positions[posA]);
return normal.Normalized;
}
// TODO : Make it work with any polygon (other than triangle)
public double GetFaceArea(int nodeIndex)
{
int posA = nodes[nodeIndex].position;
int posB = nodes[nodes[nodeIndex].relative].position;
int posC = nodes[nodes[nodes[nodeIndex].relative].relative].position;
Vector3 normal = Vector3.Cross(
positions[posB] - positions[posA],
positions[posC] - positions[posA]);
return 0.5 * normal.Length;
}
// Only works with triangles !
public double GetAngleRadians(int nodeIndex)
{
int posA = nodes[nodeIndex].position;
int posB = nodes[nodes[nodeIndex].relative].position;
int posC = nodes[nodes[nodes[nodeIndex].relative].relative].position;
return Vector3.AngleRadians(
positions[posB] - positions[posA],
positions[posC] - positions[posA]);
}
public void Compact()
{
// Rebuild nodes array with only valid nodes
{
int validNodesCount = 0;
for (int i = 0; i < nodes.Length; i++)
if (!nodes[i].IsRemoved)
validNodesCount++;
Node[] newNodes = new Node[validNodesCount];
int k = 0;
Dictionary<int, int> oldToNewNodeIndex = new Dictionary<int, int>();
for (int i = 0; i < nodes.Length; i++)
{
if (!nodes[i].IsRemoved)
{
newNodes[k] = nodes[i];
oldToNewNodeIndex.Add(i, k);
k++;
}
}
for (int i = 0; i < newNodes.Length; i++)
{
newNodes[i].relative = oldToNewNodeIndex[newNodes[i].relative];
newNodes[i].sibling = oldToNewNodeIndex[newNodes[i].sibling];
}
nodes = newNodes;
}
// Remap positions
{
Dictionary<int, int> oldToNewPosIndex = new Dictionary<int, int>();
for (int i = 0; i < nodes.Length; i++)
{
if (!oldToNewPosIndex.ContainsKey(nodes[i].position))
oldToNewPosIndex.Add(nodes[i].position, oldToNewPosIndex.Count);
nodes[i].position = oldToNewPosIndex[nodes[i].position];
}
Vector3[] newPositions = new Vector3[oldToNewPosIndex.Count];
foreach (KeyValuePair<int, int> oldToNewPos in oldToNewPosIndex)
{
newPositions[oldToNewPos.Value] = positions[oldToNewPos.Key];
}
positions = newPositions;
}
// Remap attributes
if (attributes != null)
{
Dictionary<int, int> oldToNewAttrIndex = new Dictionary<int, int>();
for (int i = 0; i < nodes.Length; i++)
{
if (!oldToNewAttrIndex.ContainsKey(nodes[i].attribute))
oldToNewAttrIndex.Add(nodes[i].attribute, oldToNewAttrIndex.Count);
nodes[i].attribute = oldToNewAttrIndex[nodes[i].attribute];
}
MetaAttributeList newAttributes = attributes.CreateNew(oldToNewAttrIndex.Count);
foreach (KeyValuePair<int, int> oldToNewAttr in oldToNewAttrIndex)
{
newAttributes[oldToNewAttr.Value] = attributes[oldToNewAttr.Key];
}
attributes = newAttributes;
}
_positionToNode = null; // Invalid now
}
public void MergePositions(double tolerance = 0.01)
{
Dictionary<Vector3, int> newPositions = new Dictionary<Vector3, int>(tolerance <= 0 ? null : new Vector3Comparer(tolerance));
for (int i = 0; i < positions.Length; i++)
{
newPositions.TryAdd(positions[i], newPositions.Count);
}
for (int i = 0; i < nodes.Length; i++)
{
nodes[i].position = newPositions[positions[nodes[i].position]];
}
positions = new Vector3[newPositions.Count];
foreach (KeyValuePair<Vector3, int> pair in newPositions)
{
positions[pair.Value] = pair.Key;
}
newPositions = null;
// Remapping siblings
Dictionary<int, int> posToLastSibling = new Dictionary<int, int>();
for (int i = 0; i < nodes.Length; i++)
{
if (posToLastSibling.ContainsKey(nodes[i].position))
{
nodes[i].sibling = posToLastSibling[nodes[i].position];
posToLastSibling[nodes[i].position] = i;
}
else
{
nodes[i].sibling = -1;
posToLastSibling.Add(nodes[i].position, i);
}
}
for (int i = 0; i < nodes.Length; i++)
{
if (nodes[i].sibling < 0)
{
// Assign last sibling to close sibling loop
nodes[i].sibling = posToLastSibling[nodes[i].position];
}
}
_positionToNode = null;
// Dereference faces that no longer exist
for (int i = 0; i < nodes.Length; i++)
{
if (nodes[i].IsRemoved)
{
continue;
}
int lastPos = nodes[i].position;
int relative = i;
while ((relative = nodes[relative].relative) != i) // Circulate around face
{
int currPos = nodes[relative].position;
if (lastPos == currPos)
{
RemoveFace(relative);
break;
}
lastPos = currPos;
}
}
}
public void MergeAttributes()
{
Dictionary<IMetaAttribute, int> _uniqueAttributes = new Dictionary<IMetaAttribute, int>();
for (int i = 0; i < nodes.Length; i++)
{
_uniqueAttributes.TryAdd(attributes[nodes[i].attribute], nodes[i].attribute);
}
for (int i = 0; i < nodes.Length; i++)
{
nodes[i].attribute = _uniqueAttributes[attributes[nodes[i].attribute]];
}
}
public void RemoveFace(int nodeIndex)
{
int relative = nodeIndex;
do
{
nodes[relative].MarkRemoved();
ReconnectSiblings(relative);
} while ((relative = nodes[relative].relative) != nodeIndex);
_faceCount--;
}
public void Scale(double factor)
{
for (int i = 0; i < positions.Length; i++)
{
positions[i] = positions[i] * factor;
}
}
public HashSet<Edge> GetAllEdges()
{
HashSet<Edge> edges = new HashSet<Edge>();
for (int p = 0; p < PositionToNode.Length; p++)
{
int nodeIndex = PositionToNode[p];
if (nodeIndex < 0)
{
continue;
}
int sibling = nodeIndex;
do
{
int firstRelative = nodes[sibling].relative;
int secondRelative = nodes[firstRelative].relative;
Edge pair = new Edge(nodes[firstRelative].position, nodes[secondRelative].position);
edges.Add(pair);
} while ((sibling = nodes[sibling].sibling) != nodeIndex);
}
return edges;
}
public SharedMesh ToSharedMesh()
{
// Compating here is an issue if mesh is being decimated :/
//Compact();
SharedMesh mesh = new SharedMesh();
List<int> triangles = new List<int>();
HashSet<int> browsedNodes = new HashSet<int>();
Group[] newGroups = new Group[groups?.Length ?? 0];
mesh.groups = newGroups;
mesh.attributeDefinitions = attributeDefinitions;
int currentGroup = 0;
int indicesInGroup = 0;
Dictionary<(int, int), int> perVertexMap = new Dictionary<(int, int), int>();
for (int i = 0; i < nodes.Length; i++)
{
if (newGroups.Length > 0 && groups[currentGroup].firstIndex == i)
{
if (currentGroup > 0)
{
newGroups[currentGroup - 1].indexCount = indicesInGroup;
newGroups[currentGroup].firstIndex = indicesInGroup + newGroups[currentGroup - 1].firstIndex;
}
indicesInGroup = 0;
if (currentGroup < groups.Length - 1)
{
currentGroup++;
}
}
if (nodes[i].IsRemoved)
{
continue;
}
indicesInGroup++;
if (browsedNodes.Contains(i))
{
continue;
}
// Only works if all elements are triangles
int relative = i;
do
{
if (browsedNodes.Add(relative) && !nodes[relative].IsRemoved)
{
(int position, int attribute) key = (nodes[relative].position, nodes[relative].attribute);
perVertexMap.TryAdd(key, perVertexMap.Count);
triangles.Add(perVertexMap[key]);
}
} while ((relative = nodes[relative].relative) != i);
}
if (newGroups.Length > 0)
{
newGroups[currentGroup].indexCount = indicesInGroup;
}
// Positions
mesh.positions = new Vector3[perVertexMap.Count];
foreach (KeyValuePair<(int, int), int> mapping in perVertexMap)
{
mesh.positions[mapping.Value] = positions[mapping.Key.Item1];
}
// Attributes
if (attributes != null && attributeDefinitions.Length > 0)
{
mesh.attributes = attributes.CreateNew(perVertexMap.Count);
foreach (KeyValuePair<(int, int), int> mapping in perVertexMap)
{
mesh.attributes[mapping.Value] = attributes[mapping.Key.Item2];
}
}
mesh.triangles = triangles.ToArray();
return mesh;
}
}
public struct Edge : IEquatable<Edge>
{
public int posA;
public int posB;
public Edge(int posA, int posB)
{
this.posA = posA;
this.posB = posB;
}
public override int GetHashCode()
{
unchecked
{
return posA + posB;
}
}
public override bool Equals(object obj)
{
return Equals((Edge)obj);
}
public bool Equals(Edge pc)
{
if (ReferenceEquals(this, pc))
{
return true;
}
else
{
return (posA == pc.posA && posB == pc.posB) || (posA == pc.posB && posB == pc.posA);
}
}
public static bool operator ==(Edge x, Edge y)
{
return x.Equals(y);
}
public static bool operator !=(Edge x, Edge y)
{
return !x.Equals(y);
}
public override string ToString()
{
return $"<A:{posA} B:{posB}>";
}
}
}