using System.Collections.Generic; using UnityEngine; using UnityEngine.Rendering; /// Concentric curtains using Omnissiah's procedural fog and the meadow's bend. [ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(215)] public sealed class PortalFogRings : MonoBehaviour { [SerializeField] private Material fogMaterial; [SerializeField, Range(1, 8)] private int layers = 4; [SerializeField, Range(0.1f, 0.9f)] private float innerRadius = 0.35f; [SerializeField, Range(0.2f, 1.2f)] private float outerRadius = 0.98f; [SerializeField, Min(1)] private float height = 8; [SerializeField] private float baseOffset = -1; private PortalMeadow meadow; private GameObject generated; private Mesh mesh; private readonly List rings = new List(); private readonly List materials = new List(); private Material lastMaterial; private bool rebuild; private void OnEnable() { meadow = GetComponent(); rebuild = true; RenderPipelineManager.beginCameraRendering += Sync; } private void OnValidate() { rebuild = true; } private void LateUpdate() { if (!meadow || !fogMaterial) { Clear(); return; } if (rebuild || !generated || lastMaterial != fogMaterial) Build(); generated.transform.localPosition = meadow.SurfaceCenter + Vector3.up * baseOffset; for (int i = 0; i < rings.Count; i++) { float t = rings.Count == 1 ? 1 : (float)i / (rings.Count - 1); float radius = meadow.Radius * Mathf.Lerp(innerRadius, Mathf.Max(innerRadius, outerRadius), t); rings[i].localScale = new Vector3(radius, Mathf.Max(1, height) * Mathf.Lerp(0.65f, 1, t), radius); var renderer = rings[i].GetComponent(); Bounds bounds = mesh.bounds; bounds.Expand(meadow.BoundsPadding * 2 / Mathf.Max(0.01f, Mathf.Min(radius, rings[i].localScale.y))); renderer.localBounds = bounds; } } private void Build() { Clear(); rebuild = false; lastMaterial = fogMaterial; generated = new GameObject("Atmospheric Fog Rings (generated)") { hideFlags = HideFlags.DontSave }; generated.transform.SetParent(transform, false); const int segments = 128, rows = 8; var vertices = new Vector3[(segments + 1) * (rows + 1)]; var triangles = new int[segments * rows * 6]; int n = 0; for (int y = 0; y <= rows; y++) for (int x = 0; x <= segments; x++) { float angle = x * Mathf.PI * 2 / segments; vertices[y * (segments + 1) + x] = new Vector3(Mathf.Sin(angle), (float)y / rows, Mathf.Cos(angle)); if (y == rows || x == segments) continue; int a = y * (segments + 1) + x, b = a + segments + 1; triangles[n++] = a; triangles[n++] = b; triangles[n++] = a + 1; triangles[n++] = a + 1; triangles[n++] = b; triangles[n++] = b + 1; } mesh = new Mesh { name = "Fog cylinder curtain", hideFlags = HideFlags.DontSave }; mesh.vertices = vertices; mesh.triangles = triangles; mesh.RecalculateBounds(); for (int i = 0; i < Mathf.Clamp(layers, 1, 8); i++) { var ring = new GameObject("Fog layer " + (i + 1)) { hideFlags = HideFlags.DontSave }; ring.transform.SetParent(generated.transform, false); ring.AddComponent().sharedMesh = mesh; var mat = new Material(fogMaterial) { name = "PortalB Fog (runtime)", hideFlags = HideFlags.DontSave }; materials.Add(mat); var renderer = ring.AddComponent(); renderer.sharedMaterial = mat; renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false; renderer.allowOcclusionWhenDynamic = false; rings.Add(ring.transform); } } private void Sync(ScriptableRenderContext context, Camera camera) { if (!meadow || !meadow.MeadowMaterial || !fogMaterial) return; for (int i = 0; i < materials.Count; i++) { var mat = materials[i]; mat.CopyPropertiesFromMaterial(fogMaterial); // From outside to inside, after the sky dome. The player is inside all curtains. mat.renderQueue = 3000 + materials.Count - 1 - i; meadow.ApplyBend(mat); meadow.ApplyPortalMask(mat); } } private void OnDisable() { RenderPipelineManager.beginCameraRendering -= Sync; Clear(); } private void Clear() { if (generated) { generated.SetActive(false); Release(generated); } foreach (Material mat in materials) if (mat) Release(mat); if (mesh) Release(mesh); generated = null; mesh = null; materials.Clear(); rings.Clear(); } private static void Release(Object obj) { if (Application.isPlaying) Destroy(obj); else DestroyImmediate(obj); } }