using T3.Core.Rendering; using T3.Core.Utils; namespace Lib.mesh.generate; [Guid("E0CEAD3C-E19C-4726-8B5C-A9FEFBF96AB9")] internal sealed class IcosahedronMesh : Instance { [Output(Guid = "9c86f704-a28f-4d2a-b7c0-15648f982463")] public readonly Slot Data = new(); public IcosahedronMesh() { Data.UpdateAction += Update; } private void Update(EvaluationContext context) { try { var scale = Scale.GetValue(context); var stretch = Stretch.GetValue(context); var pivot = Pivot.GetValue(context); var rotation = Rotation.GetValue(context); var center = Center.GetValue(context); var subdivisions = Subdivisions.GetValue(context).Clamp(0, 5); var spherical = Spherical.GetValue(context); var strength = Strength.GetValue(context); var uvMapMode = TexCoord.GetValue(context); var uvMapMode2 = TexCoord2.GetValue(context); var uvMapper = GetUvMapper(uvMapMode, subdivisions); var uvMapper2 = GetUvMapper(uvMapMode2, subdivisions); var shadingMode = Shading.GetValue(context); var yaw = rotation.Y.ToRadians(); var pitch = rotation.X.ToRadians(); var roll = rotation.Z.ToRadians(); // Apply the icosahedron tilt adjustment var rollOffset = roll - _icosahedronTiltAngle; var rotationMatrix = Matrix4x4.CreateFromYawPitchRoll(yaw, pitch, rollOffset); // Generate mesh using flat shading structure var (vertices, triangles) = GenerateIcosahedron(); if (subdivisions > 0) SubdivideMeshFlat(ref vertices, ref triangles, subdivisions, strength, spherical); if (uvMapper is Unwrapped unwrappedUv) unwrappedUv.Prepare(vertices, triangles); if (uvMapper2 is Unwrapped unwrappedUv2) unwrappedUv2.Prepare(vertices, triangles); // Calculate normals based on shading mode var normals = (shadingMode == (int)ShadingModes.Smoothed) ? CalculateSmoothNormals(vertices, triangles) : CalculateFlatNormals(vertices, triangles); // Debug: Log a few normals to compare /* if (vertices.Length >= 3) { Log.Debug($"Shading: {(shadingMode == (int)ShadingModes.Smoothed ? "Smooth" : "Flat")}"); Log.Debug($"Normal[0]: {normals[0]}"); Log.Debug($"Normal[1]: {normals[1]}"); Log.Debug($"Normal[2]: {normals[2]}"); }*/ // Create buffers if (_vertexBufferData.Length != vertices.Length) _vertexBufferData = new PbrVertex[vertices.Length]; if (_indexBufferData.Length != triangles.Length) _indexBufferData = new Int3[triangles.Length]; // Transform vertices var centerVec = new Vector3(center.X, center.Y, center.Z); var offset = new Vector3( stretch.X * scale * pivot.X, stretch.Y * scale * pivot.Y, stretch.X * scale * pivot.Z ); for (int i = 0; i < vertices.Length; i++) { var pos = new Vector3( vertices[i].X * scale * stretch.X, vertices[i].Y * scale * stretch.Y, vertices[i].Z * scale * stretch.X ); pos = Vector3.Transform(pos + offset, rotationMatrix) + centerVec; var uv = uvMapper.CalculateUV(vertices[i], normals[i], i % 3, i / 3); // Use i / 3 for triangle index var uv2 = uvMapper2.CalculateUV(vertices[i], normals[i], i % 3, i / 3); _vertexBufferData[i] = new PbrVertex { Position = pos, Normal = Vector3.TransformNormal(normals[i], rotationMatrix), Tangent = Vector3.TransformNormal(Vector3.Cross(normals[i], Vector3.UnitY), rotationMatrix), Bitangent = Vector3.TransformNormal(Vector3.Cross(normals[i], Vector3.UnitX), rotationMatrix), Texcoord = uv, Texcoord2 = uv2, Selection = 1, ColorRgb = Vector3.One, }; } // Fill index buffer for (var i = 0; i < triangles.Length; i++) { _indexBufferData[i] = new Int3( triangles[i].X, triangles[i].Y, triangles[i].Z ); } // Write Data ResourceManager.SetupStructuredBuffer(_vertexBufferData, PbrVertex.Stride * vertices.Length, PbrVertex.Stride, ref _vertexBuffer); ResourceManager.CreateStructuredBufferSrv(_vertexBuffer, ref _vertexBufferWithViews.Srv); ResourceManager.CreateStructuredBufferUav(_vertexBuffer, UnorderedAccessViewBufferFlags.None, ref _vertexBufferWithViews.Uav); _vertexBufferWithViews.Buffer = _vertexBuffer; const int stride = 3 * 4; ResourceManager.SetupStructuredBuffer(_indexBufferData, stride * triangles.Length, 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 icosahedron mesh: " + e.Message); } } private static (Vector3[] vertices, Int3[] triangles) GenerateIcosahedron() { var baseVertices = new Vector3[12]; baseVertices[0] = Vector3.Normalize(new Vector3(-1, phi, 0)); baseVertices[1] = Vector3.Normalize(new Vector3(1, phi, 0)); baseVertices[2] = Vector3.Normalize(new Vector3(-1, -phi, 0)); baseVertices[3] = Vector3.Normalize(new Vector3(1, -phi, 0)); baseVertices[4] = Vector3.Normalize(new Vector3(0, -1, phi)); baseVertices[5] = Vector3.Normalize(new Vector3(0, 1, phi)); baseVertices[6] = Vector3.Normalize(new Vector3(0, -1, -phi)); baseVertices[7] = Vector3.Normalize(new Vector3(0, 1, -phi)); baseVertices[8] = Vector3.Normalize(new Vector3(phi, 0, -1)); baseVertices[9] = Vector3.Normalize(new Vector3(phi, 0, 1)); baseVertices[10] = Vector3.Normalize(new Vector3(-phi, 0, -1)); baseVertices[11] = Vector3.Normalize(new Vector3(-phi, 0, 1)); // Original triangles (20 faces) var baseTriangles = new Int3[20]; baseTriangles[0] = new Int3(0, 11, 5); baseTriangles[1] = new Int3(0, 5, 1); baseTriangles[2] = new Int3(0, 1, 7); baseTriangles[3] = new Int3(0, 7, 10); baseTriangles[4] = new Int3(0, 10, 11); baseTriangles[5] = new Int3(5, 11, 4); baseTriangles[6] = new Int3(1, 5, 9); baseTriangles[7] = new Int3(7, 1, 8); baseTriangles[8] = new Int3(10, 7, 6); baseTriangles[9] = new Int3(11, 10, 2); baseTriangles[10] = new Int3(3, 9, 4); baseTriangles[11] = new Int3(3, 8, 9); baseTriangles[12] = new Int3(3, 6, 8); baseTriangles[13] = new Int3(3, 2, 6); baseTriangles[14] = new Int3(3, 4, 2); baseTriangles[15] = new Int3(4, 9, 5); baseTriangles[16] = new Int3(9, 8, 1); baseTriangles[17] = new Int3(8, 6, 7); baseTriangles[18] = new Int3(6, 2, 10); baseTriangles[19] = new Int3(2, 4, 11); // Split vertices for flat shading (each triangle gets its own vertices) var vertices = new List(); var triangles = new List(); foreach (var tri in baseTriangles) { var v0 = vertices.Count; vertices.Add(baseVertices[tri.X]); vertices.Add(baseVertices[tri.Y]); vertices.Add(baseVertices[tri.Z]); triangles.Add(new Int3(v0, v0 + 1, v0 + 2)); } return (vertices.ToArray(), triangles.ToArray()); } private static Vector3[] CalculateFlatNormals(Vector3[] vertices, Int3[] triangles) { var normals = new Vector3[vertices.Length]; for (var i = 0; i < triangles.Length; i++) { var tri = triangles[i]; Vector3 v1 = vertices[tri.X]; Vector3 v2 = vertices[tri.Y]; Vector3 v3 = vertices[tri.Z]; Vector3 normal = Vector3.Normalize(Vector3.Cross(v2 - v1, v3 - v1)); normals[tri.X] = normal; normals[tri.Y] = normal; normals[tri.Z] = normal; } return normals; } private static Vector3[] CalculateSmoothNormals(Vector3[] vertices, Int3[] triangles) { var normals = new Vector3[vertices.Length]; // Group vertices by position to identify duplicates var positionToIndices = new Dictionary>(new Vector3EqualityComparer()); for (var i = 0; i < vertices.Length; i++) { if (!positionToIndices.ContainsKey(vertices[i])) positionToIndices[vertices[i]] = new List(); positionToIndices[vertices[i]].Add(i); } // Calculate face normals and accumulate for each vertex position var positionNormals = new Dictionary(new Vector3EqualityComparer()); var positionTriangleCount = new Dictionary(new Vector3EqualityComparer()); for (var i = 0; i < triangles.Length; i++) { var tri = triangles[i]; var v1 = vertices[tri.X]; var v2 = vertices[tri.Y]; var v3 = vertices[tri.Z]; var normal = Vector3.Normalize(Vector3.Cross(v2 - v1, v3 - v1)); // Accumulate normal for each vertex position foreach (var v in new[] { v1, v2, v3 }) { if (!positionNormals.ContainsKey(v)) { positionNormals[v] = Vector3.Zero; positionTriangleCount[v] = 0; } positionNormals[v] += normal; positionTriangleCount[v]++; } } // Average normals per position foreach (var kvp in positionNormals) { var pos = kvp.Key; var normalSum = kvp.Value; var count = positionTriangleCount[pos]; var averagedNormal = count > 0 ? Vector3.Normalize(normalSum / count) : Vector3.UnitY; // Assign averaged normal to all vertices at this position foreach (var index in positionToIndices[pos]) { normals[index] = averagedNormal; } } return normals; } // Helper class for comparing Vector3 positions with a small tolerance private class Vector3EqualityComparer : IEqualityComparer { private const float Epsilon = 0.0001f; public bool Equals(Vector3 a, Vector3 b) { return Math.Abs(a.X - b.X) < Epsilon && Math.Abs(a.Y - b.Y) < Epsilon && Math.Abs(a.Z - b.Z) < Epsilon; } public int GetHashCode(Vector3 obj) { unchecked { int hash = 17; hash = hash * 23 + obj.X.GetHashCode(); hash = hash * 23 + obj.Y.GetHashCode(); hash = hash * 23 + obj.Z.GetHashCode(); return hash; } } } // Subdivide mesh for flat shading (each triangle gets its own vertices) private static void SubdivideMeshFlat(ref Vector3[] vertices, ref Int3[] triangles, int levels, float strength, bool spherical = true) { for (var i = 0; i < levels; i++) { var newTriangles = new List(triangles.Length * 4); var newVertices = new List(); for (var t = 0; t < triangles.Length; t++) { var v1 = vertices[triangles[t].X]; var v2 = vertices[triangles[t].Y]; var v3 = vertices[triangles[t].Z]; // Calculate midpoints (linear interpolation) var a = (v1 + v2) * 0.5f; var b = (v2 + v3) * 0.5f; var c = (v3 + v1) * 0.5f; // Add all vertices (optionally normalize) var baseIndex = newVertices.Count; newVertices.Add(spherical ? MathUtils.Lerp(v1, Vector3.Normalize(v1), strength) : v1); newVertices.Add(spherical ? MathUtils.Lerp(a, Vector3.Normalize(a), strength) : a); newVertices.Add(spherical ? MathUtils.Lerp(c, Vector3.Normalize(c), strength) : c); newVertices.Add(spherical ? MathUtils.Lerp(v2, Vector3.Normalize(v2), strength) : v2); newVertices.Add(spherical ? MathUtils.Lerp(b, Vector3.Normalize(b), strength) : b); newVertices.Add(spherical ? MathUtils.Lerp(a, Vector3.Normalize(a), strength) : a); newVertices.Add(spherical ? MathUtils.Lerp(v3, Vector3.Normalize(v3), strength) : v3); newVertices.Add(spherical ? MathUtils.Lerp(c, Vector3.Normalize(c), strength) : c); newVertices.Add(spherical ? MathUtils.Lerp(b, Vector3.Normalize(b), strength) : b); newVertices.Add(spherical ? MathUtils.Lerp(a, Vector3.Normalize(a), strength) : a); newVertices.Add(spherical ? MathUtils.Lerp(b, Vector3.Normalize(b), strength) : b); newVertices.Add(spherical ? MathUtils.Lerp(c, Vector3.Normalize(c), strength) : c); // Add new triangles (same as before) newTriangles.Add(new Int3(baseIndex + 0, baseIndex + 1, baseIndex + 2)); newTriangles.Add(new Int3(baseIndex + 3, baseIndex + 4, baseIndex + 5)); newTriangles.Add(new Int3(baseIndex + 6, baseIndex + 7, baseIndex + 8)); newTriangles.Add(new Int3(baseIndex + 9, baseIndex + 10, baseIndex + 11)); } triangles = newTriangles.ToArray(); vertices = newVertices.ToArray(); } } private IUvMapper GetUvMapper(int uvMapMode, int level) { return uvMapMode switch { 0 => new Faces(), // Standard 1 => new Unwrapped(), // Unwrapped 2 => new Atlas(level), 3 => new FacesSub(level), 4 => new GridFacesSub(level), _ => new Faces() // Default fallback }; } // Interface for UV mapping strategies private interface IUvMapper { Vector2 CalculateUV(Vector3 vertex, Vector3 normal, int vertexIndex, int triangleIndex); } private class Faces : IUvMapper { // UV coordinates that repeat every 3 vertices public Vector2 CalculateUV(Vector3 vertex, Vector3 normal, int vertexIndex, int triangleIndex) { // The pattern repeats every 3 vertices, so we can use modulo int uvIndex = vertexIndex % 3; if (uvIndex >= 0 && uvIndex < _baseUvs.Length) { return _baseUvs[uvIndex]; } // Fallback for unexpected cases Log.Warning($"Invalid UV index: {uvIndex}. Using fallback UV."); return new Vector2(0.5f, 0.5f); } } private class FacesSub(int subdivisionLevel) : IUvMapper { // Base UV coordinates for a single triangle face /* private static readonly Vector2[] _baseUvs = new Vector2[3] { new Vector2(0.5f, 1.0f), // vertex 0 (center top) new Vector2(0.067f, 0.250f), // vertex 1 (left bottom) new Vector2(0.933f, 0.250f) // vertex 2 (right bottom) };*/ private int _subdivisionLevel = subdivisionLevel; public Vector2 CalculateUV(Vector3 vertex, Vector3 normal, int vertexIndex, int triangleIndex) { if (_subdivisionLevel == 0) { // No subdivision - use base UVs var _uvIndex = vertexIndex % 3; return _baseUvs[_uvIndex]; } // Calculate which original face this triangle belongs to var subTrianglesPerFace = (int)Math.Pow(4, _subdivisionLevel); var originalFaceIndex = triangleIndex / subTrianglesPerFace; originalFaceIndex = originalFaceIndex % 20; // Ensure we don't exceed 20 faces // Calculate which sub-triangle within the original face var subTriangleIndex = triangleIndex % subTrianglesPerFace; // Get the base UV for this vertex position in the triangle var uvIndex = vertexIndex % 3; var baseUV = _baseUvs[uvIndex]; // Now we need to map this to the subdivided space // We'll use a recursive approach to find the correct sub-triangle position // Vector2 tessellatedUV = TessellateUVFace(baseUV, subTriangleIndex, _subdivisionLevel); return TessellateUV(baseUV, _baseUvs, subTriangleIndex, _subdivisionLevel); //return tessellatedUV; } } private class GridFacesSub(int subdivisionLevel) : IUvMapper // Look ma no llm! tbh I used Blender to get the base UVs for this { private readonly int _subdivisionLevel = subdivisionLevel.Clamp(0, 5); public Vector2 CalculateUV(Vector3 vertex, Vector3 normal, int vertexIndex, int triangleIndex) { /* if (_subdivisionLevel == 0) { return GetNonSubdividedUV(triangleIndex, vertexIndex); }*/ var subTrianglesPerFace = (int)Math.Pow(4, _subdivisionLevel); var originalFaceIndex = triangleIndex / subTrianglesPerFace; originalFaceIndex = originalFaceIndex % 20; var subTriangleIndex = triangleIndex % subTrianglesPerFace; var baseTriangleUvs = GetBaseTriangleUvs(originalFaceIndex); var uvIndex = vertexIndex % 3; var baseUV = baseTriangleUvs[uvIndex]; //return TessellateUVFace(baseUV, subTriangleIndex, _subdivisionLevel); return TessellateUV(baseUV, baseTriangleUvs, subTriangleIndex, _subdivisionLevel); } private Vector2[] GetBaseTriangleUvs(int originalFaceIndex) { const float cellH = 1.0f / 5; const float cellV = 1.0f / 2; var groupIndex = originalFaceIndex / 5; var faceInGroup = originalFaceIndex % 5; var xOffset = faceInGroup * cellH; xOffset += (0.2f - 0.181819f) * 0.5f; // Center the UVs horizontally if (originalFaceIndex < 5) // First group (faces 0-4) { return [ new Vector2(0.09091f + xOffset, 0.907461f), // Top center new Vector2(0.0f + xOffset, 0.75f ), // Left vertex new Vector2(0.181819f + xOffset, 0.75f), // Right vertex ]; } else if (originalFaceIndex < 10) // Second group (faces 5-9) { return [ new Vector2(0.181819f + xOffset, 0.75f), // Right vertex new Vector2(0.0f + xOffset, 0.75f), // Left vertex new Vector2(0.090911f + xOffset, 0.59254f ) // Bottom center ]; } else if (originalFaceIndex < 15) // Third group (faces 10-14) { // Apply Y shift downward (-0.157461) and X shift (+cellHWidth/2) return [ new Vector2(0.09091f + xOffset, 0.907461f- cellV), // Top center new Vector2(0.0f + xOffset, 0.75f - cellV ), // Left vertex new Vector2(0.181819f + xOffset, 0.75f- cellV), // Right vertex ]; } else // Fourth group (faces 15-19) { // Apply Y shift downward (-0.157461) and X shift (+cellHWidth/2) return [ new Vector2(0.181819f + xOffset, 0.75f- cellV), // Right vertex new Vector2(0.0f + xOffset, 0.75f - cellV), // Left vertex new Vector2(0.090911f + xOffset, 0.59254f - cellV ) // Bottom center ]; } } } private class Unwrapped : IUvMapper { private Dictionary<(int triangleIndex, int vertexIndex), Vector2> _fixedUvs; private HashSet _flippedTriangles; // Precomputes UVs with seam fix, must be called before CalculateUV public void Prepare(Vector3[] vertices, Int3[] triangles) { _fixedUvs = new Dictionary<(int, int), Vector2>(); _flippedTriangles = new HashSet(); for (var triIndex = 0; triIndex < triangles.Length; triIndex++) { var tri = triangles[triIndex]; var indices = new[] { tri.X, tri.Y, tri.Z }; var uvs = new Vector2[3]; // Step 1: Compute spherical UVs for (var i = 0; i < 3; i++) { // Match tilt rotation var tilt = Matrix4x4.CreateFromYawPitchRoll( yaw: 0f, pitch: 0f, roll: -_icosahedronTiltAngle ); // Rotate vertex into UV-mapping space var v = Vector3.Transform(vertices[indices[i]], tilt); // Then compute spherical UVs from rotated point var u = 0.5f + MathF.Atan2(v.Z, v.X) / (2 * MathF.PI); var vCoord = 0.5f + MathF.Asin(v.Y) / MathF.PI; if (u < 0f) u += 1f; if (u >= 1f) u -= 1f; uvs[i] = new Vector2(u, vCoord); } // Step 2: Seam fix var minU = MathF.Min(uvs[0].X, MathF.Min(uvs[1].X, uvs[2].X)); var maxU = MathF.Max(uvs[0].X, MathF.Max(uvs[1].X, uvs[2].X)); var wraps = (maxU - minU) > 0.5f; for (var i = 0; i < 3; i++) if (wraps && uvs[i].X < 0.5f) uvs[i].X += 1f; // Step 3: Check UV winding and flip if necessary var uvA = new Vector3(uvs[0], 0); var uvB = new Vector3(uvs[1], 0); var uvC = new Vector3(uvs[2], 0); var uvNormal = Vector3.Cross(uvB - uvA, uvC - uvA); var flipped = uvNormal.Z < 0; if (flipped) { // Flip UVs (uvs[1], uvs[2]) = (uvs[2], uvs[1]); _flippedTriangles.Add(triIndex); } // Store fixed UVs with vertex mapping order preserved (adjusted if flipped) for (int i = 0; i < 3; i++) { int fixedIndex = flipped ? (i == 1 ? 2 : i == 2 ? 1 : 0) : i; _fixedUvs[(triIndex, i)] = uvs[fixedIndex]; } } } public Vector2 CalculateUV(Vector3 vertex, Vector3 normal, int vertexIndex, int triangleIndex) { if (_fixedUvs.TryGetValue((triangleIndex, vertexIndex), out var uv)) return uv * new Vector2(-1f, 1f) + new Vector2(1f, 0f); return new Vector2(0.5f, 0.5f); // fallback } } private class Atlas(int subdivisionLevel) : IUvMapper { private readonly int _subdivisionLevel = subdivisionLevel.Clamp(0, 5); private const float currentMaxY = 0.472382f; public Vector2 CalculateUV(Vector3 vertex, Vector3 normal, int vertexIndex, int triangleIndex) { /* if (_subdivisionLevel == 0) { return GetNonSubdividedUV(triangleIndex, vertexIndex); }*/ var subTrianglesPerFace = (int)Math.Pow(4, _subdivisionLevel); var originalFaceIndex = triangleIndex / subTrianglesPerFace; originalFaceIndex = originalFaceIndex % 20; var subTriangleIndex = triangleIndex % subTrianglesPerFace; var baseTriangleUvs = GetBaseTriangleUvs(originalFaceIndex); var uvIndex = vertexIndex % 3; var baseUV = baseTriangleUvs[uvIndex]; //return TessellateUVFace(baseUV, subTriangleIndex, _subdivisionLevel); return TessellateUV(baseUV, baseTriangleUvs, subTriangleIndex, _subdivisionLevel); } private static Vector2[] GetBaseTriangleUvs(int originalFaceIndex) { const float cellWidth = 0.909091f / 5; const float yShift = 0.157461f; var groupIndex = originalFaceIndex / 5; var faceInGroup = originalFaceIndex % 5; var xOffset = faceInGroup * cellWidth; if (originalFaceIndex < 5) // First group (faces 0-4) { return [ new Vector2(0.09091f + xOffset, 1.0f), // Top vertex (0.472382 normalized) new Vector2(0.0f + xOffset, 0.314921f / currentMaxY), // Left vertex (~0.6667) new Vector2(0.181819f + xOffset, 0.314921f / currentMaxY) // Right vertex ]; } else if (originalFaceIndex < 10) // Second group (faces 5-9) { return [ new Vector2(0.181819f + xOffset, 0.314921f / currentMaxY), // Right vertex new Vector2(0.0f + xOffset, 0.314921f / currentMaxY), // Left vertex new Vector2(0.090911f + xOffset, 0.157461f / currentMaxY) // Bottom center (~0.3333) ]; } else if (originalFaceIndex < 15) // Third group (faces 10-14) { // Apply Y shift downward (-0.157461) and X shift (+cellWidth/2) return [ new Vector2(0.090911f + xOffset + cellWidth * 0.5f, (0.157461f - yShift) / currentMaxY), // Bottom center (~0.0) new Vector2(0.181819f + xOffset + cellWidth * 0.5f, (0.314921f - yShift) / currentMaxY), // Right vertex (~0.3333) new Vector2(0.0f + xOffset + cellWidth * 0.5f, (0.314921f - yShift) / currentMaxY) // Left vertex ]; } else // Fourth group (faces 15-19) { // Apply Y shift downward (-0.157461) and X shift (+cellWidth/2) return [ new Vector2(0.0f + xOffset + cellWidth * 0.5f, (0.314921f - yShift) / currentMaxY), // Left vertex (~0.3333) new Vector2(0.181819f + xOffset + cellWidth * 0.5f, (0.314921f - yShift) / currentMaxY), // Right vertex new Vector2(0.09091f + xOffset + cellWidth * 0.5f, (0.472382f - yShift) / currentMaxY) // Top center (~0.6667) ]; } } } private static Vector2 TessellateUV(Vector2 baseUV, Vector2[] baseTriangleUvs, int subTriangleIndex, int subdivisionLevel) { if (subdivisionLevel == 0) return baseUV; var currentV0 = baseTriangleUvs[0]; var currentV1 = baseTriangleUvs[1]; var currentV2 = baseTriangleUvs[2]; var currentIndex = subTriangleIndex; for (var level = subdivisionLevel; level > 0; level--) { var trianglesAtThisLevel = (int)Math.Pow(4, level - 1); var quadrant = currentIndex / trianglesAtThisLevel; currentIndex = currentIndex % trianglesAtThisLevel; var mid01 = (currentV0 + currentV1) * 0.5f; var mid12 = (currentV1 + currentV2) * 0.5f; var mid20 = (currentV2 + currentV0) * 0.5f; switch (quadrant) { case 0: currentV1 = mid01; currentV2 = mid20; break; case 1: currentV0 = currentV1; currentV1 = mid12; currentV2 = mid01; break; case 2: currentV0 = currentV2; currentV1 = mid20; currentV2 = mid12; break; case 3: currentV0 = mid01; currentV1 = mid12; currentV2 = mid20; break; } } // Determine which vertex to return based on the original baseUV if (baseUV == baseTriangleUvs[0]) return currentV0; if (baseUV == baseTriangleUvs[1]) return currentV1; if (baseUV == baseTriangleUvs[2]) return currentV2; // For midpoints, return the interpolated value return (currentV0 + currentV1 + currentV2) / 3f; } 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(); private static readonly float phi = (1f + MathF.Sqrt(5f)) / 2f; // Golden ratio, used in icosahedron generation private static readonly float _icosahedronTiltAngle = MathF.Atan(2f / (2f * phi)); // Pre-calculate the tilt angle // Base UV coordinates for a single triangle face private static readonly Vector2[] _baseUvs = [ new Vector2(0.5f, 1.0f), // vertex 0 (center top) new Vector2(0.067f, 0.250f), // vertex 11 (left bottom) new Vector2(0.933f, 0.250f) // vertex 5 (right bottom) ]; private enum UvModes { Faces, Unwrapped, Atlas, FacesSub, GridFacesSub, } private enum ShadingModes { Flat, Smoothed, } [Input(Guid = "2e8c23d8-01ac-4f53-b628-91d9ab094278")] public readonly InputSlot Subdivisions = new(); [Input(Guid = "32a77592-eaa1-43e8-b1ab-74b989ecbccd")] public readonly InputSlot Spherical = new(); [Input(Guid = "63866397-F64E-486A-8C6D-862FFD3ED42E")] public readonly InputSlot Strength = new(); [Input(Guid = "e062431e-0741-446d-ace9-e7e91080ed9f")] public readonly InputSlot Stretch = new(); [Input(Guid = "bba90ae7-689f-41d3-8a48-4f1cdb42adab")] public readonly InputSlot Scale = new(); [Input(Guid = "486c1717-20cf-4cf9-951e-cedd51c88262")] public readonly InputSlot Pivot = new(); [Input(Guid = "bbeccca7-9e1c-4702-bbd4-1cf0c9409354")] public readonly InputSlot Center = new(); [Input(Guid = "96D161DA-F459-427C-BE67-E8F1B47D233D")] public readonly InputSlot Rotation = new(); [Input(Guid = "FFD87531-8B82-4F31-9AA9-8459F92A4798", MappedType = typeof(UvModes))] public readonly InputSlot TexCoord = new(); [Input(Guid = "08dd88b7-cd91-4f17-91d9-08de5b260e7a", MappedType = typeof(UvModes))] public readonly InputSlot TexCoord2 = new(); [Input(Guid = "7438A4CA-1FA7-48CF-AD85-0E7067AE54CC", MappedType = typeof(ShadingModes))] public readonly InputSlot Shading = new(); }