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);
}
}