using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; /// Fixed concentric server rings, facing the initial meadow centre. Only the shader bends them. [ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(200)] public sealed class PortalServerGrid : MonoBehaviour { [SerializeField] private GameObject serverPrefab; [SerializeField] private Shader curvedServerShader; [Tooltip("X: minimum chord distance between neighbours. Y: distance between rings.")] [SerializeField] private Vector2 spacing = new Vector2(5, 6); [Tooltip("Empty border inside the terrain on X and Z.")] [SerializeField] private Vector2 edgeMargin = new Vector2(1, 1); [SerializeField, Min(1)] private float firstRingRadius = 8; [SerializeField, Range(0, 360)] private float angleOffset; [Tooltip("Pivot adjustment along each server's facing direction (positive = towards the centre).")] [SerializeField] private float offsetZ; [Tooltip("Additional height above the ground, after automatically aligning the model base.")] [SerializeField] private float heightOffset; [SerializeField] private Vector3 rotation = new Vector3(0, 180, 0); [Tooltip("Multiplier applied to the prefab's original scale.")] [SerializeField, Min(0.001f)] private float scaleMultiplier = 1; private PortalMeadow meadow; private GameObject generated; private Vector2 lastSize; private Vector3 lastCenter; private bool rebuild; private readonly Dictionary curvedMaterials = new Dictionary(); private readonly List servers = new List(); private float wrapExtent; // Distance between the centres of two repeated server clusters. public float RepetitionPeriod => wrapExtent * 2; private bool lastEndless; private void OnEnable() { meadow = GetComponent(); rebuild = true; RenderPipelineManager.beginCameraRendering += BeforeCameraRendering; } private void BeforeCameraRendering(ScriptableRenderContext context, Camera camera) { // Match the terrain's current material, including changes made after Update. if (meadow) SyncBend(); } private void OnValidate() { spacing.x = Mathf.Max(0.1f, spacing.x); spacing.y = Mathf.Max(0.1f, spacing.y); edgeMargin.x = Mathf.Max(0, edgeMargin.x); edgeMargin.y = Mathf.Max(0, edgeMargin.y); scaleMultiplier = Mathf.Max(0.001f, scaleMultiplier); firstRingRadius = Mathf.Max(1, firstRingRadius); rebuild = true; } private void Update() { if (!meadow) meadow = GetComponent(); if (!meadow) return; SyncBend(); if (!rebuild && lastSize == meadow.Size && lastCenter == meadow.Center && lastEndless == meadow.Endless) return; rebuild = false; lastSize = meadow.Size; lastCenter = meadow.Center; lastEndless = meadow.Endless; Build(); } private void OnDisable() { RenderPipelineManager.beginCameraRendering -= BeforeCameraRendering; Clear(); } private void LateUpdate() { if (!meadow || !meadow.Following || wrapExtent <= 0) return; Vector3 centre = meadow.SurfaceCenter; float period = wrapExtent * 2; foreach (Transform server in servers) { Vector3 p = server.localPosition; // Recycle only outside the visible disk. Nearby transforms/rotations stay fixed. p.x += Mathf.Floor((centre.x - p.x + wrapExtent) / period) * period; p.z += Mathf.Floor((centre.z - p.z + wrapExtent) / period) * period; server.localPosition = p; } } [ContextMenu("Regenerate server rings")] private void Regenerate() { rebuild = true; } private static void Release(Object value) { if (Application.isPlaying) Destroy(value); else DestroyImmediate(value); } private void Clear() { if (generated) { generated.SetActive(false); Release(generated); } generated = null; foreach (Material material in curvedMaterials.Values) if (material) Release(material); curvedMaterials.Clear(); servers.Clear(); } private GameObject CreateServer() { var instance = Instantiate(serverPrefab, generated.transform, false); instance.transform.localPosition = Vector3.zero; instance.transform.localRotation = Quaternion.Euler(rotation) * serverPrefab.transform.localRotation; instance.transform.localScale = serverPrefab.transform.localScale * scaleMultiplier; foreach (Transform child in instance.GetComponentsInChildren(true)) child.gameObject.hideFlags = HideFlags.DontSave; return instance; } private void Build() { Clear(); if (!serverPrefab) return; // Inactive during setup: avoid briefly enabling all instances at the origin. generated = new GameObject("Server Rings (generated)") { hideFlags = HideFlags.DontSave }; generated.SetActive(false); generated.transform.SetParent(transform, false); var prototype = CreateServer(); if (!TryGetLocalBounds(prototype, out Bounds bounds)) { Debug.LogWarning("Server grid: the prefab needs a Renderer to calculate its footprint.", this); Clear(); return; } float footprint = new Vector2(bounds.size.x, bounds.size.z).magnitude; wrapExtent = meadow.Radius + footprint + 1; float pitch = Mathf.Max(spacing.x, footprint, 0.1f); float ringStep = Mathf.Max(spacing.y, footprint, 0.1f); float maxRadius = Mathf.Min(lastSize.x * 0.5f - edgeMargin.x, lastSize.y * 0.5f - edgeMargin.y) - footprint * 0.5f - Mathf.Abs(offsetZ); float startRadius = Mathf.Max(firstRingRadius, footprint * 0.5f + Mathf.Abs(offsetZ)); var poses = new List(); int ring = 0; for (float radius = startRadius; radius <= maxRadius; radius += ringStep, ring++) { // Use chord length, not arc length, so neighbours cannot overlap on small rings. float placementRadius = radius - offsetZ; int around = pitch <= 2 * placementRadius ? Mathf.Max(1, Mathf.FloorToInt(Mathf.PI / Mathf.Asin(pitch / (2 * placementRadius)))) : 1; if (poses.Count + around > 4096) { Debug.LogWarning("Server rings: more than 4096 instances requested. Increase Spacing.", this); Clear(); return; } for (int i = 0; i < around; i++) { float angle = angleOffset * Mathf.Deg2Rad + (i + (ring % 2) * 0.5f) * Mathf.PI * 2 / around; Vector3 radial = new Vector3(Mathf.Sin(angle), 0, Mathf.Cos(angle)); Quaternion facing = Quaternion.LookRotation(-radial, Vector3.up); Vector3 position = lastCenter + radial * radius + facing * Vector3.forward * offsetZ; // Bounds already include the prefab rotation, scale and user rotation correction. position -= facing * new Vector3(bounds.center.x, bounds.min.y, bounds.center.z); position.y += heightOffset; poses.Add(new Pose(position, facing)); } } int count = 0; foreach (Pose pose in poses) { GameObject server = count == 0 ? prototype : CreateServer(); server.transform.localPosition = pose.position; server.transform.localRotation = pose.rotation * Quaternion.Euler(rotation) * serverPrefab.transform.localRotation; server.name = $"Server {++count:000}"; ApplyCurvedMaterials(server); servers.Add(server.transform); } if (count == 0) { Clear(); return; } SyncBend(); generated.SetActive(true); } private void ApplyCurvedMaterials(GameObject server) { if (!curvedServerShader) return; foreach (Renderer renderer in server.GetComponentsInChildren(true)) { Material[] materials = renderer.sharedMaterials; for (int i = 0; i < materials.Length; i++) { Material source = materials[i]; if (!source) continue; if (!curvedMaterials.TryGetValue(source, out Material curved)) { curved = new Material(source) { shader = curvedServerShader, name = source.name + " (Curved)", hideFlags = HideFlags.DontSave }; curved.shaderKeywords = source.shaderKeywords; curvedMaterials.Add(source, curved); } materials[i] = curved; } renderer.sharedMaterials = materials; renderer.renderingLayerMask = meadow.RenderingLayers; renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.allowOcclusionWhenDynamic = false; Bounds expanded = renderer.localBounds; Vector3 scale = renderer.transform.lossyScale; float minScale = Mathf.Max(0.001f, Mathf.Min(Mathf.Abs(scale.x), Mathf.Abs(scale.y), Mathf.Abs(scale.z))); expanded.Expand(meadow.BoundsPadding * 2 / minScale); renderer.localBounds = expanded; } } private void SyncBend() { if (!meadow.MeadowMaterial) return; foreach (Material material in curvedMaterials.Values) { meadow.ApplyBend(material); meadow.ConfigureRegion(material); } } private bool TryGetLocalBounds(GameObject instance, out Bounds bounds) { bounds = default; bool found = false; foreach (Renderer renderer in instance.GetComponentsInChildren(true)) { Bounds local = renderer.localBounds; Matrix4x4 matrix = generated.transform.worldToLocalMatrix * renderer.transform.localToWorldMatrix; for (int i = 0; i < 8; i++) { Vector3 corner = local.center + Vector3.Scale(local.extents, new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1)); Vector3 point = matrix.MultiplyPoint3x4(corner); if (!found) { bounds = new Bounds(point, Vector3.zero); found = true; } else bounds.Encapsulate(point); } } return found; } }