This commit is contained in:
2026-09-29 12:48:59 +02:00
parent b0c6111d57
commit f48e81716f
3 changed files with 211 additions and 36 deletions
+146 -3
View File
@@ -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<Plane[]> portalFrusta = new List<Plane[]>();
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<Bounds> tileLocalBounds = new List<Bounds>();
private readonly List<Mesh> meshes = new List<Mesh>();
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; }
/// <summary>Largest shader displacement inside the visible disk (the bend origin stays near its centre in Play mode).</summary>
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"));
}
/// <summary>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.</summary>
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;
}
/// <summary>Whether this bent-bounds culling applies to this camera this frame (the main camera in Play mode).</summary>
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<Portal>(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;
}
/// <summary>World bounds grown by the bend (convex, so its maximum is on a corner) and the wind.</summary>
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 };
+33 -1
View File
@@ -34,6 +34,9 @@ public sealed class PortalServerGrid : MonoBehaviour
private readonly List<Transform> servers = new List<Transform>();
private readonly List<Collider> colliders = new List<Collider>();
private bool collidersEnabled = true;
private readonly List<Renderer> serverRenderers = new List<Renderer>();
private readonly List<Bounds> serverLocalBounds = new List<Bounds>();
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);