Add main menu scene with Rive UI, procedural background, and scene transitions

- Add Menu scene with Rive-driven login and navigation UI
  (RiveLoginController, RiveNavigationController) for entering a
  username and starting/navigating the client via Rive triggers
- Add SceneTransitionController to fade out and load the Landscape
  gameplay scene when Play is selected
- Add procedural low-poly triangle field background and animated wave
  panel (LowPolyTriangleField, TriangleWavePanel) with matching
  editor menus and materials
- Add MenuLightAnimator for animated menu lighting and UI fade helpers
  (MenuFadeIn, DebugFadeToggle/Bridge)
- Rename SampleScene to Landscape and wire ChatNetworkManager to hide
  the new Rive login canvas once gameplay starts
- Bump Input System action codegen version and add Rive package
  references/project settings updates to support the new UI
This commit is contained in:
pelpanagiotis
2026-09-17 17:18:45 +03:00
parent aa248cda7e
commit d04ff998cb
101 changed files with 297098 additions and 8223 deletions
@@ -0,0 +1,304 @@
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Procedural
{
[ExecuteAlways]
[RequireComponent(typeof(MeshFilter), typeof(MeshRenderer))]
public class LowPolyTriangleField : MonoBehaviour
{
[Header("Grid")]
[Min(1)] public int columns = 24;
[Min(1)] public int rows = 14;
public float cellSize = 1.5f;
[Range(0f, 0.5f)] public float jitterAmount = 0.35f;
[Header("Static Shape")]
public float noiseScale = 0.12f;
public float heightAmplitude = 1.2f;
[Header("Wave Animation")]
public bool animate = true;
public float waveAmplitude = 0.25f;
public float waveFrequency = 0.25f;
public float waveSpeed = 0.6f;
[Header("Edges")]
public float edgeWidth = 0.06f;
public float edgeHeightOffset = 0.02f;
[Header("Materials")]
public Material faceMaterial;
public Material edgeMaterial;
[Header("Random")]
public int seed = 12345;
Mesh _mesh;
MeshFilter _meshFilter;
Vector2[] _gridXZ;
float[] _staticHeight;
Vector3[] _gridNormalAccum;
int[] _gridNormalCount;
List<int[]> _triangles;
List<(int a, int b)> _uniqueEdges;
Vector3[] _facePositions;
Vector3[] _faceNormals;
int[] _faceIndices;
Vector3[] _edgePositions;
int[] _edgeIndices;
bool _topologyDirty = true;
void OnEnable()
{
_meshFilter = GetComponent<MeshFilter>();
RebuildTopology();
UpdateGeometry(0f);
#if UNITY_EDITOR
EditorApplication.update -= EditorTick;
if (!Application.isPlaying)
EditorApplication.update += EditorTick;
#endif
}
void OnDisable()
{
#if UNITY_EDITOR
EditorApplication.update -= EditorTick;
#endif
}
void OnValidate()
{
_topologyDirty = true;
}
#if UNITY_EDITOR
void EditorTick()
{
if (Application.isPlaying) return;
if (_topologyDirty) RebuildTopology();
if (animate) UpdateGeometry((float)EditorApplication.timeSinceStartup);
if (this != null) EditorUtility.SetDirty(this);
}
#endif
void Update()
{
if (!Application.isPlaying) return;
if (_topologyDirty) RebuildTopology();
if (animate) UpdateGeometry(Time.time);
}
void RebuildTopology()
{
_topologyDirty = false;
int vertsX = columns + 1;
int vertsZ = rows + 1;
int gridCount = vertsX * vertsZ;
_gridXZ = new Vector2[gridCount];
_staticHeight = new float[gridCount];
var rng = new System.Random(seed);
float originX = -columns * cellSize * 0.5f;
float originZ = -rows * cellSize * 0.5f;
for (int j = 0; j < vertsZ; j++)
{
for (int i = 0; i < vertsX; i++)
{
int idx = j * vertsX + i;
bool border = i == 0 || i == columns || j == 0 || j == rows;
float jitterX = border ? 0f : ((float)rng.NextDouble() * 2f - 1f) * jitterAmount * cellSize;
float jitterZ = border ? 0f : ((float)rng.NextDouble() * 2f - 1f) * jitterAmount * cellSize;
float x = originX + i * cellSize + jitterX;
float z = originZ + j * cellSize + jitterZ;
_gridXZ[idx] = new Vector2(x, z);
float n = Mathf.PerlinNoise(x * noiseScale + seed * 0.017f, z * noiseScale + seed * 0.017f);
_staticHeight[idx] = (n * 2f - 1f) * heightAmplitude;
}
}
_triangles = new List<int[]>(columns * rows * 2);
var edgeSet = new HashSet<(int, int)>();
_uniqueEdges = new List<(int, int)>();
for (int j = 0; j < rows; j++)
{
for (int i = 0; i < columns; i++)
{
int i00 = j * vertsX + i;
int i10 = j * vertsX + (i + 1);
int i01 = (j + 1) * vertsX + i;
int i11 = (j + 1) * vertsX + (i + 1);
bool flip = rng.Next(2) == 0;
if (flip)
{
AddTriangle(i00, i11, i01, edgeSet);
AddTriangle(i00, i10, i11, edgeSet);
}
else
{
AddTriangle(i00, i10, i01, edgeSet);
AddTriangle(i10, i11, i01, edgeSet);
}
}
}
int faceVertCount = _triangles.Count * 3;
_facePositions = new Vector3[faceVertCount];
_faceNormals = new Vector3[faceVertCount];
_faceIndices = new int[faceVertCount];
for (int k = 0; k < faceVertCount; k++) _faceIndices[k] = k;
int edgeVertCount = _uniqueEdges.Count * 4;
_edgePositions = new Vector3[edgeVertCount];
_edgeIndices = new int[_uniqueEdges.Count * 6];
for (int e = 0; e < _uniqueEdges.Count; e++)
{
int b = e * 4;
int t = e * 6;
_edgeIndices[t + 0] = b + 0;
_edgeIndices[t + 1] = b + 2;
_edgeIndices[t + 2] = b + 1;
_edgeIndices[t + 3] = b + 1;
_edgeIndices[t + 4] = b + 2;
_edgeIndices[t + 5] = b + 3;
}
_gridNormalAccum = new Vector3[gridCount];
_gridNormalCount = new int[gridCount];
if (_mesh == null)
{
_mesh = new Mesh { name = "LowPolyTriangleField" };
_mesh.MarkDynamic();
}
_mesh.indexFormat = (faceVertCount + edgeVertCount) > 60000
? UnityEngine.Rendering.IndexFormat.UInt32
: UnityEngine.Rendering.IndexFormat.UInt16;
_mesh.Clear();
_meshFilter.sharedMesh = _mesh;
var renderer = GetComponent<MeshRenderer>();
renderer.sharedMaterials = new[] { faceMaterial, edgeMaterial };
void AddTriangle(int a, int b2, int c, HashSet<(int, int)> edges)
{
_triangles.Add(new[] { a, b2, c });
AddEdge(a, b2, edges);
AddEdge(b2, c, edges);
AddEdge(c, a, edges);
}
void AddEdge(int a, int b2, HashSet<(int, int)> edges)
{
var key = a < b2 ? (a, b2) : (b2, a);
if (edges.Add(key)) _uniqueEdges.Add(key);
}
}
void UpdateGeometry(float time)
{
if (_gridXZ == null) return;
int gridCount = _gridXZ.Length;
var pos = new Vector3[gridCount];
for (int idx = 0; idx < gridCount; idx++)
{
var xz = _gridXZ[idx];
float wave = 0f;
if (animate)
{
wave = waveAmplitude * 0.5f * (
Mathf.Sin((xz.x + xz.y) * waveFrequency + time * waveSpeed) +
Mathf.Sin((xz.x - xz.y) * waveFrequency * 0.7f + time * waveSpeed * 1.3f));
}
pos[idx] = new Vector3(xz.x, _staticHeight[idx] + wave, xz.y);
_gridNormalAccum[idx] = Vector3.zero;
_gridNormalCount[idx] = 0;
}
for (int t = 0; t < _triangles.Count; t++)
{
var tri = _triangles[t];
Vector3 v0 = pos[tri[0]];
Vector3 v1 = pos[tri[1]];
Vector3 v2 = pos[tri[2]];
Vector3 normal = Vector3.Cross(v1 - v0, v2 - v0).normalized;
int b = t * 3;
_facePositions[b + 0] = v0;
_facePositions[b + 1] = v1;
_facePositions[b + 2] = v2;
_faceNormals[b + 0] = normal;
_faceNormals[b + 1] = normal;
_faceNormals[b + 2] = normal;
_gridNormalAccum[tri[0]] += normal; _gridNormalCount[tri[0]]++;
_gridNormalAccum[tri[1]] += normal; _gridNormalCount[tri[1]]++;
_gridNormalAccum[tri[2]] += normal; _gridNormalCount[tri[2]]++;
}
for (int e = 0; e < _uniqueEdges.Count; e++)
{
var (a, bIdx) = _uniqueEdges[e];
Vector3 p0 = pos[a];
Vector3 p1 = pos[bIdx];
Vector3 n0 = _gridNormalCount[a] > 0 ? (_gridNormalAccum[a] / _gridNormalCount[a]).normalized : Vector3.up;
Vector3 n1 = _gridNormalCount[bIdx] > 0 ? (_gridNormalAccum[bIdx] / _gridNormalCount[bIdx]).normalized : Vector3.up;
Vector3 dir = (p1 - p0).normalized;
Vector3 avgNormal = (n0 + n1).normalized;
Vector3 side = Vector3.Cross(dir, avgNormal).normalized * (edgeWidth * 0.5f);
Vector3 p0Off = p0 + n0 * edgeHeightOffset;
Vector3 p1Off = p1 + n1 * edgeHeightOffset;
int b = e * 4;
_edgePositions[b + 0] = p0Off - side;
_edgePositions[b + 1] = p0Off + side;
_edgePositions[b + 2] = p1Off - side;
_edgePositions[b + 3] = p1Off + side;
}
_mesh.subMeshCount = 2;
_mesh.SetVertices(_facePositions.Length > 0 ? Concat(_facePositions, _edgePositions) : _edgePositions);
int faceVertCount = _facePositions.Length;
var normals = new Vector3[faceVertCount + _edgePositions.Length];
for (int i = 0; i < faceVertCount; i++) normals[i] = _faceNormals[i];
for (int i = 0; i < _edgePositions.Length; i++) normals[faceVertCount + i] = Vector3.up;
_mesh.SetNormals(normals);
_mesh.SetIndices(_faceIndices, MeshTopology.Triangles, 0, false);
var offsetEdgeIndices = new int[_edgeIndices.Length];
for (int i = 0; i < _edgeIndices.Length; i++) offsetEdgeIndices[i] = _edgeIndices[i] + faceVertCount;
_mesh.SetIndices(offsetEdgeIndices, MeshTopology.Triangles, 1, false);
_mesh.RecalculateBounds();
}
static Vector3[] Concat(Vector3[] a, Vector3[] b)
{
var result = new Vector3[a.Length + b.Length];
a.CopyTo(result, 0);
b.CopyTo(result, a.Length);
return result;
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: d98243f3b31761c45a022724b517c3df
@@ -0,0 +1,394 @@
using System.Collections.Generic;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif
namespace Procedural
{
/// <summary>
/// Builds a panel out of individual equilateral-triangle GameObjects tiled edge-to-edge.
/// Each tile is its own mesh/transform; a shared driver moves every tile's transform
/// independently each frame so the panel reads as a wave, rather than deforming one mesh.
/// </summary>
[ExecuteAlways]
public class TriangleWavePanel : MonoBehaviour
{
[Header("Grid")]
[Min(1)] public int columns = 14;
[Min(1)] public int rows = 10;
public float triangleSize = 1.2f;
[Range(0.5f, 1f)] public float gapScale = 1f;
[Header("Volume & Corner Chamfer")]
public float thickness = 0.18f;
[Range(0f, 0.4f)] public float cornerRadius = 0f;
[Range(1, 8)] public int cornerSegments = 1;
[Header("Wave")]
public bool animate = true;
public float waveAmplitude = 0.22f;
public float waveFrequency = 0.35f;
public float waveSpeed = 1.1f;
[Range(0f, 20f)] public float tiltAmount = 5f;
[Header("Panel Sway (whole-panel rotation)")]
[Range(0f, 45f)] public float swayAmplitude = 0f;
public float swaySpeed = 0.2f;
[Header("Backer (fills the gaps)")]
public bool showBacker = true;
public float backerOffset = -0.4f;
public float backerPadding = 1.5f;
[Header("Materials")]
public Material faceMaterial;
public Material backerMaterial;
Transform[] _tileTransforms;
Vector3[] _tileBasePos;
Quaternion _baseRotation;
bool _dirty = true;
void OnEnable()
{
_baseRotation = transform.localRotation;
Rebuild();
#if UNITY_EDITOR
EditorApplication.update -= EditorTick;
if (!Application.isPlaying)
EditorApplication.update += EditorTick;
#endif
}
void OnDisable()
{
#if UNITY_EDITOR
EditorApplication.update -= EditorTick;
#endif
}
void OnValidate()
{
_dirty = true;
}
/// <summary>
/// Forces an immediate rebuild. Needed after configuring fields via code (e.g. from an
/// editor script right after AddComponent), since plain field writes don't route through
/// OnValidate the way Inspector edits do, so _dirty never flips back on by itself.
/// </summary>
public void RebuildNow()
{
_baseRotation = transform.localRotation;
Rebuild();
}
#if UNITY_EDITOR
void EditorTick()
{
if (Application.isPlaying) return;
if (_dirty) Rebuild();
if (animate) AnimateTiles((float)EditorApplication.timeSinceStartup);
}
#endif
void Update()
{
if (!Application.isPlaying) return;
if (_dirty) Rebuild();
if (animate) AnimateTiles(Time.time);
}
void Rebuild()
{
_dirty = false;
ClearChildren();
float s = triangleSize;
float h = s * Mathf.Sqrt(3f) * 0.5f;
Mesh upMesh = BuildRoundedTrianglePrism(
new Vector2(-s * 0.5f, -h / 3f),
new Vector2(s * 0.5f, -h / 3f),
new Vector2(0f, 2f * h / 3f),
gapScale, cornerRadius, cornerSegments, thickness, "TrianglePanel_Up");
Mesh downMesh = BuildRoundedTrianglePrism(
new Vector2(-s * 0.5f, h / 3f),
new Vector2(s * 0.5f, h / 3f),
new Vector2(0f, -2f * h / 3f),
gapScale, cornerRadius, cornerSegments, thickness, "TrianglePanel_Down");
float originX = -(columns * s) * 0.5f;
float originZ = -(rows * h) * 0.5f;
var transforms = new List<Transform>(columns * rows * 2);
var basePositions = new List<Vector3>(columns * rows * 2);
for (int r = 0; r < rows; r++)
{
for (int c = 0; c < columns; c++)
{
float rowZ = originZ + r * h;
Vector3 upPos = new Vector3(originX + c * s + s * 0.5f, 0f, rowZ + h / 3f);
CreateTile($"Tile_{r}_{c}_Up", upMesh, upPos, transforms, basePositions);
Vector3 downPos = new Vector3(originX + c * s + s, 0f, rowZ + 2f * h / 3f);
CreateTile($"Tile_{r}_{c}_Down", downMesh, downPos, transforms, basePositions);
}
}
_tileTransforms = transforms.ToArray();
_tileBasePos = basePositions.ToArray();
if (showBacker)
BuildBacker(originX, originZ, s, h);
AnimateTiles(0f);
}
void CreateTile(string tileName, Mesh mesh, Vector3 localPos, List<Transform> transforms, List<Vector3> basePositions)
{
var go = new GameObject(tileName);
go.transform.SetParent(transform, false);
go.transform.localPosition = localPos;
go.AddComponent<MeshFilter>().sharedMesh = mesh;
var mr = go.AddComponent<MeshRenderer>();
mr.sharedMaterial = faceMaterial;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
transforms.Add(go.transform);
basePositions.Add(localPos);
}
void BuildBacker(float originX, float originZ, float s, float h)
{
var go = new GameObject("Backer");
go.transform.SetParent(transform, false);
float width = columns * s + backerPadding * 2f;
float depth = rows * h + backerPadding * 2f;
float cx = originX + (columns * s) * 0.5f;
float cz = originZ + (rows * h) * 0.5f;
go.transform.localPosition = new Vector3(cx, backerOffset, cz);
var mesh = new Mesh { name = "TrianglePanel_Backer" };
float hw = width * 0.5f;
float hd = depth * 0.5f;
mesh.vertices = new[]
{
new Vector3(-hw, 0f, -hd),
new Vector3(hw, 0f, -hd),
new Vector3(-hw, 0f, hd),
new Vector3(hw, 0f, hd),
};
mesh.triangles = new[] { 0, 2, 1, 1, 2, 3 };
mesh.normals = new[] { Vector3.up, Vector3.up, Vector3.up, Vector3.up };
mesh.RecalculateBounds();
go.AddComponent<MeshFilter>().sharedMesh = mesh;
var mr = go.AddComponent<MeshRenderer>();
mr.sharedMaterial = backerMaterial;
mr.shadowCastingMode = UnityEngine.Rendering.ShadowCastingMode.Off;
}
static Mesh BuildRoundedTrianglePrism(Vector2 a, Vector2 b, Vector2 c, float gapScale, float requestedRadius,
int cornerSegments, float thickness, string meshName)
{
Vector2[] pts = { a * gapScale, b * gapScale, c * gapScale };
float minEdge = Mathf.Min(
Vector2.Distance(pts[0], pts[1]),
Mathf.Min(Vector2.Distance(pts[1], pts[2]), Vector2.Distance(pts[2], pts[0])));
float radius = Mathf.Clamp(requestedRadius, 0f, minEdge * 0.28f);
List<Vector2> outline = RoundedTriangleOutline(pts, radius, cornerSegments);
int n = outline.Count;
float halfT = thickness * 0.5f;
var vertices = new List<Vector3>(n * 4 + 2);
var normals = new List<Vector3>(n * 4 + 2);
var triangles = new List<int>(n * 12);
int topCenter = vertices.Count;
vertices.Add(new Vector3(0f, halfT, 0f));
normals.Add(Vector3.up);
int topStart = vertices.Count;
for (int i = 0; i < n; i++)
{
vertices.Add(new Vector3(outline[i].x, halfT, outline[i].y));
normals.Add(Vector3.up);
}
for (int i = 0; i < n; i++)
{
triangles.Add(topCenter);
triangles.Add(topStart + i);
triangles.Add(topStart + (i + 1) % n);
}
int bottomCenter = vertices.Count;
vertices.Add(new Vector3(0f, -halfT, 0f));
normals.Add(Vector3.down);
int bottomStart = vertices.Count;
for (int i = 0; i < n; i++)
{
vertices.Add(new Vector3(outline[i].x, -halfT, outline[i].y));
normals.Add(Vector3.down);
}
for (int i = 0; i < n; i++)
{
triangles.Add(bottomCenter);
triangles.Add(bottomStart + (i + 1) % n);
triangles.Add(bottomStart + i);
}
int sideTopStart = vertices.Count;
for (int i = 0; i < n; i++)
{
Vector2 p = outline[i];
Vector3 outward = new Vector3(p.x, 0f, p.y);
outward = outward.sqrMagnitude > 0.0001f ? outward.normalized : Vector3.forward;
vertices.Add(new Vector3(p.x, halfT, p.y));
normals.Add(outward);
}
int sideBottomStart = vertices.Count;
for (int i = 0; i < n; i++)
{
Vector2 p = outline[i];
Vector3 outward = new Vector3(p.x, 0f, p.y);
outward = outward.sqrMagnitude > 0.0001f ? outward.normalized : Vector3.forward;
vertices.Add(new Vector3(p.x, -halfT, p.y));
normals.Add(outward);
}
for (int i = 0; i < n; i++)
{
int next = (i + 1) % n;
int t0 = sideTopStart + i;
int t1 = sideTopStart + next;
int b0 = sideBottomStart + i;
int b1 = sideBottomStart + next;
triangles.Add(t0); triangles.Add(b0); triangles.Add(t1);
triangles.Add(t1); triangles.Add(b0); triangles.Add(b1);
}
var mesh = new Mesh { name = meshName };
mesh.SetVertices(vertices);
mesh.SetNormals(normals);
mesh.SetTriangles(triangles, 0);
mesh.RecalculateBounds();
return mesh;
}
static List<Vector2> RoundedTriangleOutline(Vector2[] pts, float radius, int segments)
{
var outline = new List<Vector2>();
for (int k = 0; k < 3; k++)
{
Vector2 v = pts[k];
if (radius < 0.0001f)
{
outline.Add(v);
continue;
}
Vector2 prev = pts[(k + 2) % 3];
Vector2 next = pts[(k + 1) % 3];
Vector2 dirToPrev = (prev - v).normalized;
Vector2 dirToNext = (next - v).normalized;
float dot = Mathf.Clamp(Vector2.Dot(dirToPrev, dirToNext), -1f, 1f);
float angle = Mathf.Acos(dot);
float halfAngle = angle * 0.5f;
float tangentDist = radius / Mathf.Tan(halfAngle);
float centerDist = radius / Mathf.Sin(halfAngle);
Vector2 bisector = dirToPrev + dirToNext;
bisector = bisector.sqrMagnitude > 0.0001f ? bisector.normalized : dirToPrev;
Vector2 tangentOnPrevEdge = v + dirToPrev * tangentDist;
Vector2 tangentOnNextEdge = v + dirToNext * tangentDist;
Vector2 center = v + bisector * centerDist;
Vector2 startDir = (tangentOnPrevEdge - center).normalized;
Vector2 endDir = (tangentOnNextEdge - center).normalized;
float cross = startDir.x * endDir.y - startDir.y * endDir.x;
float sweepSign = cross >= 0f ? 1f : -1f;
float sweepAngle = Mathf.Acos(Mathf.Clamp(Vector2.Dot(startDir, endDir), -1f, 1f));
int segs = Mathf.Max(1, segments);
for (int seg = 0; seg <= segs; seg++)
{
float t = (float)seg / segs;
float a = sweepSign * sweepAngle * t;
float ca = Mathf.Cos(a), sa = Mathf.Sin(a);
Vector2 rotated = new Vector2(
startDir.x * ca - startDir.y * sa,
startDir.x * sa + startDir.y * ca);
outline.Add(center + rotated * radius);
}
}
return outline;
}
void AnimateTiles(float time)
{
if (swayAmplitude > 0.001f && animate)
{
float sway = Mathf.Sin(time * swaySpeed) * swayAmplitude;
// Rotate around the world Y axis on top of whatever base orientation the panel
// was authored with (e.g. tipped vertical), rather than a local axis, so the sway
// always reads as a gentle turn regardless of how the panel itself is oriented.
transform.localRotation = Quaternion.AngleAxis(sway, Vector3.up) * _baseRotation;
}
else
{
transform.localRotation = _baseRotation;
}
if (_tileTransforms == null) return;
float t = time * waveSpeed;
for (int i = 0; i < _tileTransforms.Length; i++)
{
var tf = _tileTransforms[i];
if (tf == null) continue;
var b = _tileBasePos[i];
float sum = (b.x + b.z) * waveFrequency;
float diff = (b.x - b.z) * waveFrequency * 0.7f;
float wave = animate
? waveAmplitude * 0.5f * (Mathf.Sin(sum + t) + Mathf.Sin(diff + t * 1.3f))
: 0f;
tf.localPosition = new Vector3(b.x, b.y + wave, b.z);
if (tiltAmount > 0f && animate)
{
float tiltX = tiltAmount * Mathf.Cos(diff + t * 1.3f);
float tiltZ = tiltAmount * Mathf.Cos(sum + t);
tf.localRotation = Quaternion.Euler(tiltX, 0f, -tiltZ);
}
else
{
tf.localRotation = Quaternion.identity;
}
}
}
void ClearChildren()
{
for (int i = transform.childCount - 1; i >= 0; i--)
{
var child = transform.GetChild(i).gameObject;
if (Application.isPlaying) Destroy(child);
else DestroyImmediate(child);
}
}
}
}
@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5476e1f1daded4d4f82d503bb03be2d3