using UnityEngine; using UnityEngine.Rendering; /// A local sky, independent of RenderSettings.skybox, ready for the existing projection masks. [ExecuteAlways, DisallowMultipleComponent, RequireComponent(typeof(PortalMeadow)), DefaultExecutionOrder(210)] public sealed class PortalSkyDome : MonoBehaviour { public enum SkyVisibility { Preview, PortalOnly, Hidden } [SerializeField] private Material skyMaterial; [SerializeField] private SkyVisibility visibility = SkyVisibility.Preview; [SerializeField] private bool insidePortalB; [Tooltip("The sphere also expands automatically to contain the terrain and its bend bounds.")] [SerializeField, Min(10)] private float minimumRadius = 100; private PortalMeadow meadow; private GameObject dome; private Mesh mesh; private Material runtimeMaterial; private Material sourceMaterial; private void OnEnable() { meadow = GetComponent(); } private void LateUpdate() { if (!meadow) meadow = GetComponent(); if (!meadow || !skyMaterial) { Clear(); return; } if (!dome || sourceMaterial != skyMaterial) Build(); // The centre is the same initial snapshot used by the terrain and the server rings. dome.transform.localPosition = meadow.SurfaceCenter; float radius = Mathf.Max(minimumRadius, meadow.Size.magnitude + meadow.BoundsPadding + 20); dome.transform.localScale = Vector3.one * radius; runtimeMaterial.CopyPropertiesFromMaterial(skyMaterial); runtimeMaterial.SetFloat("_Visibility", (float)visibility); runtimeMaterial.SetFloat("_InsidePortalB", insidePortalB ? 1 : 0); } // Ready to connect to the future portal's enter/exit UnityEvents. No distance trigger is inferred. public void EnterPortalB() { SetInsidePortalB(true); } public void ExitPortalB() { SetInsidePortalB(false); } public void SetInsidePortalB(bool inside) { insidePortalB = inside; visibility = SkyVisibility.PortalOnly; if (runtimeMaterial) { runtimeMaterial.SetFloat("_Visibility", (float)visibility); runtimeMaterial.SetFloat("_InsidePortalB", inside ? 1 : 0); } } private void Build() { Clear(); sourceMaterial = skyMaterial; runtimeMaterial = new Material(skyMaterial) { name = "PortalB Sky (runtime)", hideFlags = HideFlags.DontSave }; dome = new GameObject("PortalB Sky Sphere (generated)") { hideFlags = HideFlags.DontSave, layer = gameObject.layer }; dome.transform.SetParent(transform, false); const int segments = 64, rings = 32; var positions = new Vector3[(segments + 1) * (rings + 1)]; var triangles = new int[segments * rings * 6]; for (int y = 0; y <= rings; y++) for (int x = 0; x <= segments; x++) { float latitude = Mathf.PI * y / rings, longitude = 2 * Mathf.PI * x / segments; positions[y * (segments + 1) + x] = new Vector3(Mathf.Sin(latitude) * Mathf.Cos(longitude), Mathf.Cos(latitude), Mathf.Sin(latitude) * Mathf.Sin(longitude)); } int index = 0; for (int y = 0; y < rings; y++) for (int x = 0; x < segments; x++) { int a = y * (segments + 1) + x, b = a + segments + 1; triangles[index++] = a; triangles[index++] = a + 1; triangles[index++] = b; triangles[index++] = b; triangles[index++] = a + 1; triangles[index++] = b + 1; } mesh = new Mesh { name = "Portal sky sphere", hideFlags = HideFlags.DontSave }; mesh.vertices = positions; mesh.triangles = triangles; mesh.RecalculateBounds(); dome.AddComponent().sharedMesh = mesh; var renderer = dome.AddComponent(); renderer.sharedMaterial = runtimeMaterial; renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.receiveShadows = false; renderer.allowOcclusionWhenDynamic = false; } private void OnDisable() { Clear(); } private void Clear() { if (dome) { dome.SetActive(false); Release(dome); } if (mesh) Release(mesh); if (runtimeMaterial) Release(runtimeMaterial); dome = null; mesh = null; runtimeMaterial = null; sourceMaterial = null; } private static void Release(Object value) { if (Application.isPlaying) Destroy(value); else DestroyImmediate(value); } }