diff --git a/Assets/_Content/Scenes/Samyaza.unity b/Assets/_Content/Scenes/Samyaza.unity index 86a757e9..c78018ce 100644 --- a/Assets/_Content/Scenes/Samyaza.unity +++ b/Assets/_Content/Scenes/Samyaza.unity @@ -868,7 +868,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 36794108} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -1326,7 +1326,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 59807273} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4591,7 +4591,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 206805645} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -6228,7 +6228,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 272190441} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -7048,7 +7048,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 310902350} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -7323,7 +7323,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 322804685} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4362435,7 +4362435,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 359837277} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4362525,7 +4362525,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 362198025} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4362953,7 +4362953,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 375752585} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4365583,7 +4365583,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 508314613} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4369458,7 +4369458,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 704139311} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4370374,7 +4370374,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 766138167} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4370677,7 +4370677,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 771592285} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4371047,7 +4371047,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 791728122} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4373500,7 +4373500,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 927451500} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4378259,7 +4378259,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1141410561} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4378990,7 +4378990,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1159401206} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4379170,7 +4379170,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1168463803} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4380061,7 +4380061,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1245722682} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4384446,7 +4384446,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1405303267} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4384881,7 +4384881,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1436216804} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4385834,7 +4385834,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1481080613} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4386441,7 +4386441,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1499419166} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4387547,7 +4387547,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1530425818} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4388162,7 +4388162,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1561024980} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4388943,7 +4388943,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1607149165} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4389123,7 +4389123,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1611230947} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4395211,7 +4395211,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1905332969} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4395695,7 +4395695,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1924599564} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4396454,7 +4396454,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 1949588682} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4398048,7 +4398048,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2013619295} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 @@ -4398352,7 +4398352,7 @@ MeshRenderer: m_PrefabInstance: {fileID: 0} m_PrefabAsset: {fileID: 0} m_GameObject: {fileID: 2022650010} - m_Enabled: 1 + m_Enabled: 0 m_CastShadows: 1 m_ReceiveShadows: 1 m_DynamicOccludee: 1 diff --git a/Assets/_Content/Scripts/PortalMeadow.cs b/Assets/_Content/Scripts/PortalMeadow.cs index 00f4130f..fc7dfea0 100644 --- a/Assets/_Content/Scripts/PortalMeadow.cs +++ b/Assets/_Content/Scripts/PortalMeadow.cs @@ -25,7 +25,8 @@ public sealed class PortalMeadow : MonoBehaviour [SerializeField] private Vector2 bladeHeight = new Vector2(0.25f, 0.65f); [SerializeField, Range(0.01f, 0.15f)] private float bladeWidth = 0.055f; [SerializeField] private int seed = 1978; - [Tooltip("Extra bounds in every direction to accommodate camera-relative shader bending.")] + [Tooltip("Maximum extra bounds in every direction to accommodate camera-relative shader bending. " + + "In Play mode the padding is reduced to the real bend of the visible disk, so renderers can be culled.")] [SerializeField, Min(1)] private float boundsPadding = 128; [Tooltip("Rendering layers of every generated PortalB renderer, so lights can be restricted to this world.")] [SerializeField] private RenderingLayerMask renderingLayers = 1u; @@ -41,6 +42,23 @@ public sealed class PortalMeadow : MonoBehaviour private static readonly int FrameFixedId = Shader.PropertyToID("_CurvedWorldFrameFixed"); private float frameBlend; + // Culling with the real bent bounds (the padded renderer bounds are too large for Unity to cull anything): + // before the crossing PortalB is only visible through the portal openings, then through the camera frustum. + private Portal[] portals; + private readonly List portalFrusta = new List(); + private readonly Plane[] cameraFrustum = new Plane[6]; + private int portalFrustaCount; + private bool throughPortals; + private int frustaFrame = -1; + private Camera frustaCamera; + private bool culling; + private bool tilesCulled; + private float cullCurvature, cullStart, cullHorizontal; + private Vector2 cullAxis; + private Vector3 cullBendDirection, cullFramePosition; + private Quaternion cullFrameRotation; + private readonly List tileLocalBounds = new List(); + private readonly List meshes = new List(); private GameObject generated; private bool rebuild; @@ -59,7 +77,7 @@ public sealed class PortalMeadow : MonoBehaviour public Vector2 Size => size; public Vector3 Center => center + (centerOnPlayerAtStart ? initialCenterOffset : Vector3.zero); public Material MeadowMaterial => material; - public float BoundsPadding => boundsPadding; + public float BoundsPadding => Application.isPlaying ? Mathf.Min(boundsPadding, MaxBendDisplacement() + 2) : boundsPadding; public uint RenderingLayers => renderingLayers; public float Radius => Mathf.Min(size.x, size.y) * 0.5f; public bool Endless => endless && Application.isPlaying; @@ -95,8 +113,129 @@ public sealed class PortalMeadow : MonoBehaviour public void SetFollowPlayer(bool follow) { followPlayer = follow; } + /// Largest shader displacement inside the visible disk (the bend origin stays near its centre in Play mode). + private float MaxBendDisplacement() + { + if (!material || !material.HasProperty("_Curvature")) return boundsPadding; + // The disk plus the wrapped servers just past its edge. + float reach = Radius + 8; + float distance = Mathf.Max(0, reach - material.GetFloat("_StartDistance")); + float horizontal = material.HasProperty("_HorizontalInfluence") ? material.GetFloat("_HorizontalInfluence") : 0; + return (distance * distance + reach * reach * horizontal) * Mathf.Abs(material.GetFloat("_Curvature")); + } + + /// True when these (unbent) world bounds cannot be seen by the camera once bent by the shader: + /// outside the portal openings before the crossing, outside the camera frustum afterwards. + public bool IsCulled(Bounds worldBounds, Camera camera) + { + if (!UpdateCulling(camera)) return false; + Bounds bent = BendBounds(worldBounds); + if (!throughPortals) return !GeometryUtility.TestPlanesAABB(cameraFrustum, bent); + for (int i = 0; i < portalFrustaCount; i++) + if (GeometryUtility.TestPlanesAABB(portalFrusta[i], bent)) return false; + return true; + } + + /// Whether this bent-bounds culling applies to this camera this frame (the main camera in Play mode). + public bool Culls(Camera camera) { return UpdateCulling(camera); } + + private bool UpdateCulling(Camera camera) + { + if (frustaFrame == Time.frameCount && frustaCamera == camera) return culling; + frustaFrame = Time.frameCount; + frustaCamera = camera; + portalFrustaCount = 0; + culling = Application.isPlaying && material && material.HasProperty("_Curvature") && camera && camera == Camera.main; + if (!culling) return false; + + // Same bend frame as the shaders: the anchor before the crossing, blending to the camera afterwards. + Transform view = camera.transform; + float blend = bendAnchor ? frameBlend : 1; + cullFramePosition = bendAnchor ? Vector3.Lerp(bendAnchor.position, view.position, blend) : view.position; + cullFrameRotation = bendAnchor ? Quaternion.Slerp(bendAnchor.rotation, view.rotation, blend) : view.rotation; + cullCurvature = Mathf.Abs(material.GetFloat("_Curvature")); + cullStart = material.GetFloat("_StartDistance"); + cullHorizontal = material.HasProperty("_HorizontalInfluence") ? material.GetFloat("_HorizontalInfluence") : 0; + float angle = material.GetFloat("_BendAngle") * Mathf.Deg2Rad; + cullAxis = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)); + cullBendDirection = cullFrameRotation * new Vector3(Mathf.Sin(angle), -Mathf.Cos(angle), 0); + + throughPortals = !Following && bendAnchor; + if (!throughPortals) + { + GeometryUtility.CalculateFrustumPlanes(camera, cameraFrustum); + return true; + } + if (portals == null) portals = FindObjectsByType(FindObjectsSortMode.None); + Vector3 eye = camera.transform.position; + foreach (Portal portal in portals) + { + if (!portal || !portal.isActiveAndEnabled) continue; + Transform t = portal.transform; + // A portal opens onto PortalB along its forward axis, only for a camera in front of it. + if (Vector3.Dot(t.position - eye, t.forward) <= 0) continue; + if (portalFrustaCount == portalFrusta.Count) portalFrusta.Add(new Plane[5]); + Plane[] planes = portalFrusta[portalFrustaCount++]; + Vector3 right = t.right * (t.localScale.x * 0.5f), up = t.up * (t.localScale.y * 0.5f); + Vector3 inside = t.position + (t.position - eye).normalized; + Vector3 a = t.position - right - up, b = t.position - right + up, c = t.position + right + up, d = t.position + right - up; + planes[0] = SidePlane(eye, a, b, inside); + planes[1] = SidePlane(eye, b, c, inside); + planes[2] = SidePlane(eye, c, d, inside); + planes[3] = SidePlane(eye, d, a, inside); + planes[4] = new Plane(t.forward, t.position); + } + return true; + } + + private static Plane SidePlane(Vector3 eye, Vector3 a, Vector3 b, Vector3 inside) + { + var plane = new Plane(eye, a, b); + if (!plane.GetSide(inside)) plane.Flip(); + return plane; + } + + /// World bounds grown by the bend (convex, so its maximum is on a corner) and the wind. + private Bounds BendBounds(Bounds bounds) + { + bounds.Expand(1); + Quaternion toFrame = Quaternion.Inverse(cullFrameRotation); + Vector3 origin = cullFramePosition; + float maxBend = 0; + for (int i = 0; i < 8; i++) + { + Vector3 corner = bounds.center + Vector3.Scale(bounds.extents, + new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1)); + Vector3 local = toFrame * (corner - origin); + float distance = Mathf.Max(0, local.z - cullStart); + float lateral = local.x * cullAxis.x + local.y * cullAxis.y; + maxBend = Mathf.Max(maxBend, (distance * distance + lateral * lateral * cullHorizontal) * cullCurvature); + } + Vector3 offset = cullBendDirection * maxBend; + bounds.Encapsulate(bounds.min + offset); + bounds.Encapsulate(bounds.max + offset); + return bounds; + } + + private void CullTiles(Camera camera) + { + if (camera != Camera.main) return; + bool cull = Culls(camera); + if (!cull && !tilesCulled) return; + tilesCulled = cull; + for (int i = 0; i < tileRenderers.Count; i++) + { + if (!tileRenderers[i].enabled) continue; + Transform tile = tiles[i]; + Bounds local = tileLocalBounds[i]; + var world = new Bounds(tile.TransformPoint(local.center), Vector3.Scale(local.size, tile.lossyScale)); + tileRenderers[i].forceRenderingOff = cull && IsCulled(world, camera); + } + } + private void BeforeCameraRendering(ScriptableRenderContext context, Camera camera) { + CullTiles(camera); if (!bendAnchor || frameBlend >= 1) { Shader.SetGlobalFloat(FrameFixedId, 0); return; } Transform view = camera.transform; Vector3 position = Vector3.Lerp(bendAnchor.position, view.position, frameBlend); @@ -208,6 +347,9 @@ public sealed class PortalMeadow : MonoBehaviour meshes.Clear(); tiles.Clear(); tileRenderers.Clear(); + tileLocalBounds.Clear(); + tilesCulled = false; + portals = null; if (renderedMaterial) Release(renderedMaterial); renderedMaterial = null; } @@ -297,7 +439,8 @@ public sealed class PortalMeadow : MonoBehaviour mesh.SetColors(colors); mesh.SetTriangles(triangles, 0); mesh.RecalculateBounds(); Bounds bounds = mesh.bounds; - bounds.Expand(boundsPadding * 2); + tileLocalBounds.Add(bounds); + bounds.Expand(BoundsPadding * 2); mesh.bounds = bounds; meshes.Add(mesh); var tile = new GameObject($"Meadow {tx},{tz}") { hideFlags = HideFlags.DontSave, layer = gameObject.layer }; diff --git a/Assets/_Content/Scripts/PortalServerGrid.cs b/Assets/_Content/Scripts/PortalServerGrid.cs index c9c9f8e3..cfdbc9c8 100644 --- a/Assets/_Content/Scripts/PortalServerGrid.cs +++ b/Assets/_Content/Scripts/PortalServerGrid.cs @@ -34,6 +34,9 @@ public sealed class PortalServerGrid : MonoBehaviour private readonly List servers = new List(); private readonly List colliders = new List(); private bool collidersEnabled = true; + private readonly List serverRenderers = new List(); + private readonly List serverLocalBounds = new List(); + private bool serversCulled; private float wrapExtent; // Distance between the centres of two repeated server clusters. public float RepetitionPeriod => wrapExtent * 2; @@ -48,7 +51,31 @@ public sealed class PortalServerGrid : MonoBehaviour private void BeforeCameraRendering(ScriptableRenderContext context, Camera camera) { // Match the terrain's current material, including changes made after Update. - if (meadow) SyncBend(); + if (!meadow) return; + SyncBend(); + CullServers(camera); + } + private void CullServers(Camera camera) + { + if (camera != Camera.main) return; + bool cull = meadow.Culls(camera); + if (!cull && !serversCulled) return; + serversCulled = cull; + for (int i = 0; i < serverRenderers.Count; i++) + { + Renderer renderer = serverRenderers[i]; + if (!renderer) continue; + renderer.forceRenderingOff = cull && meadow.IsCulled(WorldBounds(renderer, serverLocalBounds[i]), camera); + } + } + private static Bounds WorldBounds(Renderer renderer, Bounds local) + { + Matrix4x4 matrix = renderer.localToWorldMatrix; + var bounds = new Bounds(matrix.MultiplyPoint3x4(local.center), Vector3.zero); + for (int i = 0; i < 8; i++) + bounds.Encapsulate(matrix.MultiplyPoint3x4(local.center + Vector3.Scale(local.extents, + new Vector3((i & 1) == 0 ? -1 : 1, (i & 2) == 0 ? -1 : 1, (i & 4) == 0 ? -1 : 1)))); + return bounds; } private void OnValidate() { @@ -115,6 +142,9 @@ public sealed class PortalServerGrid : MonoBehaviour curvedMaterials.Clear(); servers.Clear(); colliders.Clear(); + serverRenderers.Clear(); + serverLocalBounds.Clear(); + serversCulled = false; } private GameObject CreateServer() { @@ -224,6 +254,8 @@ public sealed class PortalServerGrid : MonoBehaviour renderer.shadowCastingMode = ShadowCastingMode.Off; renderer.allowOcclusionWhenDynamic = false; Bounds expanded = renderer.localBounds; + serverRenderers.Add(renderer); + serverLocalBounds.Add(expanded); 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);