Files
2026-07-13 13:13:17 +08:00

727 lines
27 KiB
C#

using T3.Core.Rendering;
using T3.Core.Utils;
using DelaunatorSharp;
namespace Lib.mesh.generate;
[Guid("bf4daa46-ed0f-4a87-9ba1-93631b2ca29a")]
internal sealed class DelaunayMesh : Instance<DelaunayMesh>
{
[Output(Guid = "6c85e367-f91c-4f3d-9d3d-e422a521e3a9")]
public readonly Slot<MeshBuffers> Data = new();
public DelaunayMesh()
{
Data.UpdateAction += Update;
}
private void Update(EvaluationContext context)
{
var pointList = BoundaryPoints.GetValue(context);
var extraPointList = ExtraPoints.GetValue(context);
var tweak = Tweak.GetValue(context);
var seed = Seed.GetValue(context);
var subdivideLongEdges = SubdivideLongEdges.GetValue(context);
// Get fill density parameter
// Note: Inverted so that 1 means full fill and 0 means no fill for the user
var fillDensity = 1 - FillDensity.GetValue(context);
try
{
// Get the point list from input
if (pointList == null || pointList.NumElements == 0)
{
Log.Warning("DelaunayMesh: No points in list");
return;
}
// Cast to StructuredList<Point> to access TypedElements
var typedPointList = pointList as StructuredList<Point>;
if (typedPointList == null)
{
Log.Error("DelaunayMesh: List is not of type StructuredList<Point>");
return;
}
var originalPointArray = typedPointList.TypedElements;
// Calculate hash of the boundary points array for cache invalidation
int currentHash = CalculateBoundaryPointsHash(originalPointArray);
// Determine if we need to reprocess the boundary points
bool needsReprocessing = !_hasProcessedOnce || // First evaluation after load (CRITICAL!)
_cachedFilteredBoundaryPoints == null || // Cache empty
currentHash != _cachedBoundaryPointsHash; // Hash changed
Point[] pointArray;
if (needsReprocessing)
{
// Filter out points with NaN values
var validPoints = new List<Point>();
int invalidCount = 0;
for (int i = 0; i < originalPointArray.Length; i++)
{
var point = originalPointArray[i];
var scaleNaN = point.Scale;
// Check for NaN in Scale
if (float.IsNaN(scaleNaN.X) || float.IsNaN(scaleNaN.Y) || float.IsNaN(scaleNaN.Z))
{
invalidCount++;
continue;
}
validPoints.Add(point);
}
if (invalidCount > 0)
{
Log.Debug($"DelaunayMesh: Filtered out {invalidCount} points with NaN Scale values");
}
pointArray = [.. validPoints];
// Update cache
_cachedFilteredBoundaryPoints = pointArray;
_cachedBoundaryPointsHash = currentHash;
_hasProcessedOnce = true;
}
else
{
// Use cached filtered points
pointArray = _cachedFilteredBoundaryPoints;
// Removed the debug log that was spamming the console
}
if (pointArray.Length < 3)
{
Log.Warning("DelaunayMesh: Need at least 3 valid points for triangulation");
return;
}
// Subdivide long boundary edges for better triangulation
var subdividedPoints = new List<Point>();
var maxEdgeSubdivisionLength = 0f;
if (subdivideLongEdges > 0)
{
maxEdgeSubdivisionLength = 1 - subdivideLongEdges; // Subdivide edges longer than 1.5x the fill density
}
for (int i = 0; i < pointArray.Length; i++)
{
var currentPoint = pointArray[i];
var nextPoint = pointArray[(i + 1) % pointArray.Length];
subdividedPoints.Add(currentPoint);
// Calculate edge length
var edgeLength = Vector2.Distance(
new Vector2(currentPoint.Position.X, currentPoint.Position.Y),
new Vector2(nextPoint.Position.X, nextPoint.Position.Y));
// Subdivide if edge is too long
if (edgeLength > maxEdgeSubdivisionLength && maxEdgeSubdivisionLength > 0.0001f)
{
int subdivisions = (int)Math.Ceiling(edgeLength / maxEdgeSubdivisionLength);
for (int j = 1; j < subdivisions; j++)
{
float t = (float)j / subdivisions;
var interpolatedPos = Vector3.Lerp(currentPoint.Position, nextPoint.Position, t);
var interpolatedColor = Vector4.Lerp(currentPoint.Color, nextPoint.Color, t);
subdividedPoints.Add(new Point
{
Position = interpolatedPos,
F1 = 1,
Orientation = Quaternion.Identity,
Color = interpolatedColor,
Scale = Vector3.One,
F2 = 1
});
}
}
}
pointArray = subdividedPoints.ToArray();
// Store original boundary points before adding fill points
var originalBoundaryCount = pointArray.Length;
var boundaryPolygon = new Vector2[originalBoundaryCount];
for (int i = 0; i < originalBoundaryCount; i++)
{
boundaryPolygon[i] = new Vector2(pointArray[i].Position.X, pointArray[i].Position.Y);
}
// Create spatial grid for boundary points
float gridCellSize = fillDensity * 2f; // Adjust based on your needs
var boundaryGrid = new BoundaryGrid(boundaryPolygon, gridCellSize);
// Generate additional points inside the boundary using Poisson disc sampling
var allPoints = new List<Point>(pointArray);
// FIXED: Changed condition to check if fillDensity is above minimum threshold
if (fillDensity < 0.9999f)
{
// Calculate bounds for the boundary using a single loop (more efficient than LINQ)
var minXb = float.MaxValue;
var maxXb = float.MinValue;
var minYb = float.MaxValue;
var maxYb = float.MinValue;
for (int i = 0; i < pointArray.Length; i++)
{
var pos = pointArray[i].Position;
if (pos.X < minXb) minXb = pos.X;
if (pos.X > maxXb) maxXb = pos.X;
if (pos.Y < minYb) minYb = pos.Y;
if (pos.Y > maxYb) maxYb = pos.Y;
}
// Generate Poisson disc samples with seed
var fillPoints = GeneratePoissonDiscSamples(minXb, maxXb, minYb, maxYb, fillDensity, seed);
// Filter out points that are:
// 1. Not inside the boundary polygon
// 2. Too close to boundary points (to avoid edge artifacts)
var minDistanceFromBoundary = fillDensity * tweak; // Keep some margin from boundary
var validFillPoints = new List<Vector2>();
foreach (var fillPoint in fillPoints)
{
// Check if inside boundary (still need polygon test)
if (!IsPointInPolygon(fillPoint, boundaryPolygon))
continue;
// Use spatial grid for distance check (much faster!)
if (!boundaryGrid.IsTooCloseToBoundary(fillPoint, minDistanceFromBoundary, boundaryPolygon))
{
validFillPoints.Add(fillPoint);
}
}
// Add valid fill points to the point array
foreach (var fillPoint in validFillPoints)
{
allPoints.Add(new Point
{
Position = new Vector3(fillPoint.X, fillPoint.Y, 0),
F1 = 1,
Orientation = Quaternion.Identity,
Color = Vector4.One,
Scale = Vector3.One,
F2 = 1
});
}
}
else
{
// Use ExtraPoints list when fillDensity is very low (user's FillDensity > 0.9999)
if (extraPointList != null && extraPointList.NumElements > 0)
{
// Cast to StructuredList<Point> to access TypedElements
var typedExtraPointList = extraPointList as StructuredList<Point>;
if (typedExtraPointList != null)
{
var extraPointArray = typedExtraPointList.TypedElements;
// Filter extra points similarly to Poisson disc samples
var minDistanceFromBoundary = fillDensity * tweak; // Use same margin as for fill points
// Add valid extra points to the allPoints list
for (int i = 0; i < extraPointArray.Length; i++)
{
var point = extraPointArray[i];
// Skip points with NaN values
if (float.IsNaN(point.Scale.X) || float.IsNaN(point.Scale.Y) || float.IsNaN(point.Scale.Z))
continue;
var pointPos2D = new Vector2(point.Position.X, point.Position.Y);
// Check if point is inside boundary polygon
if (!IsPointInPolygon(pointPos2D, boundaryPolygon))
continue;
// Check if point is too close to boundary (using the spatial grid for efficiency)
if (boundaryGrid.IsTooCloseToBoundary(pointPos2D, minDistanceFromBoundary, boundaryPolygon))
continue;
allPoints.Add(point);
}
}
else
{
Log.Warning("DelaunayMesh: ExtraPoints list is not of type StructuredList<Point>");
}
}
}
// Use the combined point array for triangulation
pointArray = allPoints.ToArray();
// Convert Point array to IPoint array for Delaunator (only x,y coordinates)
var delaunatorPoints = pointArray.Select(p => new DelaunatorSharp.Point(p.Position.X, p.Position.Y) as IPoint).ToArray();
// Perform Delaunay triangulation
var delaunay = new Delaunator(delaunatorPoints);
// Get vertices and triangles count
var verticesCount = pointArray.Length;
var triangleCount = delaunay.Triangles.Length / 3;
// Calculate bounds for UV mapping
var minX = float.MaxValue;
var maxX = float.MinValue;
var minY = float.MaxValue;
var maxY = float.MinValue;
foreach (var point in pointArray)
{
var x = point.Position.X;
var y = point.Position.Y;
if (x < minX) minX = x;
if (x > maxX) maxX = x;
if (y < minY) minY = y;
if (y > maxY) maxY = y;
}
var rangeX = maxX - minX;
var rangeY = maxY - minY;
// Avoid division by zero for UV calculation
if (rangeX < 0.0001f) rangeX = 1.0f;
if (rangeY < 0.0001f) rangeY = 1.0f;
// Create vertices with transformations
if (_vertexBufferData.Length != verticesCount)
{
_vertexBufferData = new PbrVertex[verticesCount];
}
for (int i = 0; i < verticesCount; i++)
{
var point = pointArray[i];
var pos = point.Position;
var color = new Vector3( point.Color.X, point.Color.Y, point.Color.Z);
// Calculate UV coordinates (normalized 0-1 based on point positions)
var u = (pos.X - minX) / rangeX;
var v = (pos.Y - minY) / rangeY;
var uv = new Vector2(u, v);
_vertexBufferData[i] = new PbrVertex
{
Position = pos,
Normal = new Vector3(0, 0, 1),
Tangent = new Vector3(1, 0, 0),
Bitangent = new Vector3(0, 1, 0),
Texcoord = uv,
Selection = point.F1,
ColorRgb = color,
};
}
// Filter triangles based on edge length and boundary
var validTriangles = new List<Int3>();
for (int i = 0; i < triangleCount; i++)
{
var idx0 = delaunay.Triangles[i * 3];
var idx1 = delaunay.Triangles[i * 3 + 1];
var idx2 = delaunay.Triangles[i * 3 + 2];
var p0 = pointArray[idx0].Position;
var p1 = pointArray[idx1].Position;
var p2 = pointArray[idx2].Position;
bool keepTriangle = true;
// Boundary filtering - check if triangle centroid is inside boundary polygon
if (keepTriangle)
{
var centroid = new Vector2(
(p0.X + p1.X + p2.X) / 3f,
(p0.Y + p1.Y + p2.Y) / 3f
);
if (!IsPointInPolygon(centroid, boundaryPolygon))
{
keepTriangle = false;
}
}
if (keepTriangle)
{
validTriangles.Add(new Int3(idx0, idx2, idx1)); // Reversed winding order for Tixl's back-face culling
}
}
var faceCount = validTriangles.Count;
// Create index buffer
if (_indexBufferData.Length != faceCount)
{
_indexBufferData = new Int3[faceCount];
}
for (int i = 0; i < faceCount; i++)
{
_indexBufferData[i] = validTriangles[i];
}
// Update GPU buffers
var stride = PbrVertex.Stride;
ResourceManager.SetupStructuredBuffer(_vertexBufferData, stride * verticesCount, stride, ref _vertexBuffer);
ResourceManager.CreateStructuredBufferSrv(_vertexBuffer, ref _vertexBufferWithViews.Srv);
ResourceManager.CreateStructuredBufferUav(_vertexBuffer, UnorderedAccessViewBufferFlags.None, ref _vertexBufferWithViews.Uav);
_vertexBufferWithViews.Buffer = _vertexBuffer;
stride = 3 * sizeof(int);
ResourceManager.SetupStructuredBuffer(_indexBufferData, stride * faceCount, stride, ref _indexBuffer);
ResourceManager.CreateStructuredBufferSrv(_indexBuffer, ref _indexBufferWithViews.Srv);
ResourceManager.CreateStructuredBufferUav(_indexBuffer, UnorderedAccessViewBufferFlags.None, ref _indexBufferWithViews.Uav);
_indexBufferWithViews.Buffer = _indexBuffer;
_data.VertexBuffer = _vertexBufferWithViews;
_data.IndicesBuffer = _indexBufferWithViews;
Data.Value = _data;
Data.DirtyFlag.Clear();
}
catch (Exception e)
{
Log.Error("Failed to create Delaunay mesh: " + e.Message);
}
}
// Point-in-polygon test using ray casting algorithm
private static bool IsPointInPolygon(Vector2 point, Vector2[] polygon)
{
var inside = false;
var n = polygon.Length;
for (int i = 0, j = n - 1; i < n; j = i++)
{
if ((polygon[i].Y > point.Y) != (polygon[j].Y > point.Y) &&
point.X < (polygon[j].X - polygon[i].X) * (point.Y - polygon[i].Y) / (polygon[j].Y - polygon[i].Y) + polygon[i].X)
{
inside = !inside;
}
}
return inside;
}
// Generate Poisson disc samples inside the boundary polygon
private static List<Vector2> GeneratePoissonDiscSamples(float minX, float maxX, float minY, float maxY, float radius, int seed)
{
var samples = new List<Vector2>();
var activeList = new List<Vector2>();
var random = new System.Random(seed); // FIXED: Use seed for deterministic generation
// Grid cell size
var cellSize = radius / MathF.Sqrt(2);
var gridWidth = (int)MathF.Ceiling((maxX - minX) / cellSize);
var gridHeight = (int)MathF.Ceiling((maxY - minY) / cellSize);
// Grid to track occupied cells (-1 = empty, >= 0 = index in samples list)
var grid = new int[gridWidth * gridHeight];
for (int i = 0; i < grid.Length; i++)
grid[i] = -1;
// Helper function to get grid index
int GetGridIndex(float x, float y)
{
int gridX = (int)((x - minX) / cellSize);
int gridY = (int)((y - minY) / cellSize);
if (gridX < 0 || gridX >= gridWidth || gridY < 0 || gridY >= gridHeight)
return -1;
return gridX + gridY * gridWidth;
}
// Start with the center point of the rectangle
Vector2 firstPoint = new Vector2(
minX + (maxX - minX) * 0.5f,
minY + (maxY - minY) * 0.5f
);
samples.Add(firstPoint);
activeList.Add(firstPoint);
int gridIdx = GetGridIndex(firstPoint.X, firstPoint.Y);
if (gridIdx >= 0)
grid[gridIdx] = 0;
// Process active list
int maxAttempts = 30; // Standard Poisson disc parameter
while (activeList.Count > 0)
{
int randomIndex = random.Next(activeList.Count);
Vector2 point = activeList[randomIndex];
bool foundCandidate = false;
for (int i = 0; i < maxAttempts; i++)
{
// Generate random point around the active point
float angle = (float)(random.NextDouble() * 2 * MathF.PI);
float distance = radius + (float)(random.NextDouble() * radius);
float newX = point.X + distance * MathF.Cos(angle);
float newY = point.Y + distance * MathF.Sin(angle);
var newPoint = new Vector2(newX, newY);
// Check if point is within bounds and inside boundary
if (newX < minX || newX >= maxX || newY < minY || newY >= maxY)
continue;
// Check if point is far enough from all other points
int newGridIdx = GetGridIndex(newX, newY);
if (newGridIdx < 0)
continue;
bool tooClose = false;
// Pre-calculate squared radius (avoid square root in distance checks)
float radiusSquared = radius * radius;
// Check neighboring grid cells
int gridX = (int)((newX - minX) / cellSize);
int gridY = (int)((newY - minY) / cellSize);
for (int dy = -2; dy <= 2; dy++)
{
for (int dx = -2; dx <= 2; dx++)
{
int checkX = gridX + dx;
int checkY = gridY + dy;
if (checkX < 0 || checkX >= gridWidth || checkY < 0 || checkY >= gridHeight)
continue;
int checkIdx = checkX + checkY * gridWidth;
int sampleIdx = grid[checkIdx];
if (sampleIdx >= 0)
{
Vector2 sample = samples[sampleIdx];
float dxVal = newPoint.X - sample.X;
float dyVal = newPoint.Y - sample.Y;
float distSquared = dxVal * dxVal + dyVal * dyVal;
if (distSquared < radiusSquared)
{
tooClose = true;
break;
}
}
}
if (tooClose) break;
}
if (!tooClose)
{
samples.Add(newPoint);
activeList.Add(newPoint);
grid[newGridIdx] = samples.Count - 1;
foundCandidate = true;
break;
}
}
if (!foundCandidate)
{
activeList.RemoveAt(randomIndex);
}
}
return samples;
}
// Calculate a simple hash of the boundary points array for cache invalidation
private static int CalculateBoundaryPointsHash(Point[] points)
{
if (points == null || points.Length == 0)
return 0;
unchecked
{
int hash = 17;
hash = hash * 31 + points.Length;
// Sample points for hash calculation to avoid performance issues with large arrays
// Hash first, middle, and last points, plus array length
if (points.Length > 0)
{
hash = hash * 31 + HashPoint(points[0]);
}
if (points.Length > 1)
{
hash = hash * 31 + HashPoint(points[points.Length - 1]);
}
if (points.Length > 2)
{
hash = hash * 31 + HashPoint(points[points.Length / 2]);
}
return hash;
}
}
// Helper to hash a single Point's key properties
private static int HashPoint(Point point)
{
unchecked
{
int hash = 17;
hash = hash * 31 + point.Position.GetHashCode();
hash = hash * 31 + point.Scale.GetHashCode();
return hash;
}
}
private class BoundaryGrid
{
private readonly float _minX, _minY, _maxX, _maxY;
private readonly float _cellSize;
private readonly int _gridWidth, _gridHeight;
private readonly List<int>[,] _grid; // Stores indices of boundary points in each cell
public BoundaryGrid(Vector2[] boundaryPoints, float cellSize)
{
_cellSize = cellSize;
// Calculate bounds
_minX = float.MaxValue;
_minY = float.MaxValue;
_maxX = float.MinValue;
_maxY = float.MinValue;
foreach (var point in boundaryPoints)
{
if (point.X < _minX) _minX = point.X;
if (point.X > _maxX) _maxX = point.X;
if (point.Y < _minY) _minY = point.Y;
if (point.Y > _maxY) _maxY = point.Y;
}
// Add small padding to handle points exactly on bounds
_minX -= 0.001f;
_minY -= 0.001f;
_maxX += 0.001f;
_maxY += 0.001f;
_gridWidth = (int)Math.Ceiling((_maxX - _minX) / _cellSize);
_gridHeight = (int)Math.Ceiling((_maxY - _minY) / _cellSize);
// Initialize grid
_grid = new List<int>[_gridWidth, _gridHeight];
for (int x = 0; x < _gridWidth; x++)
for (int y = 0; y < _gridHeight; y++)
_grid[x, y] = new List<int>();
// Add points to grid
for (int i = 0; i < boundaryPoints.Length; i++)
{
var point = boundaryPoints[i];
int gridX = (int)((point.X - _minX) / _cellSize);
int gridY = (int)((point.Y - _minY) / _cellSize);
// Clamp to grid bounds (shouldn't happen with padding)
gridX = Math.Clamp(gridX, 0, _gridWidth - 1);
gridY = Math.Clamp(gridY, 0, _gridHeight - 1);
_grid[gridX, gridY].Add(i);
}
}
// Check if a point is too close to any boundary point
public bool IsTooCloseToBoundary(Vector2 point, float minDistance, Vector2[] boundaryPoints)
{
float minDistanceSquared = minDistance * minDistance;
// Determine which grid cell this point falls into
int gridX = (int)((point.X - _minX) / _cellSize);
int gridY = (int)((point.Y - _minY) / _cellSize);
// Check neighboring cells (including current cell)
int startX = Math.Max(0, gridX - 1);
int endX = Math.Min(_gridWidth - 1, gridX + 1);
int startY = Math.Max(0, gridY - 1);
int endY = Math.Min(_gridHeight - 1, gridY + 1);
for (int x = startX; x <= endX; x++)
{
for (int y = startY; y <= endY; y++)
{
foreach (int index in _grid[x, y])
{
Vector2 boundaryPoint = boundaryPoints[index];
float dx = point.X - boundaryPoint.X;
float dy = point.Y - boundaryPoint.Y;
float distSquared = dx * dx + dy * dy;
if (distSquared < minDistanceSquared)
return true;
}
}
}
return false;
}
}
private Buffer _vertexBuffer;
private PbrVertex[] _vertexBufferData = new PbrVertex[0];
private readonly BufferWithViews _vertexBufferWithViews = new();
private Buffer _indexBuffer;
private Int3[] _indexBufferData = new Int3[0];
private readonly BufferWithViews _indexBufferWithViews = new();
private readonly MeshBuffers _data = new();
// Caching for NaN-filtered boundary points
private Point[] _cachedFilteredBoundaryPoints = null;
private int _cachedBoundaryPointsHash = 0;
private bool _hasProcessedOnce = false;
[Input(Guid = "18FDDD63-DB79-4EE6-9A32-B90A5CEFF582")]
public readonly InputSlot<StructuredList> BoundaryPoints = new();
[Input(Guid = "DB3C69B1-403B-485B-94E8-FC7E8B566947")]
public readonly InputSlot<StructuredList> ExtraPoints = new();
[Input(Guid = "ABA31520-065F-40C7-A4A6-A4470F1E0CDF")]
public readonly InputSlot<float> SubdivideLongEdges = new();
[Input(Guid = "e00e4b12-8576-4a78-b773-17630b102a70")]
public readonly InputSlot<float> FillDensity = new();
[Input(Guid = "3236E937-9DBE-41E8-AAFA-C0C13C56BCDF")]
public readonly InputSlot<int> Seed = new();
[Input(Guid = "0B30E8F2-44D7-41DB-B38B-E6A053B1AEBA")]
public readonly InputSlot<float> Tweak = new();
}