From e43f2d74557dcbf2cc408893c7b7d7ac708a44a8 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Wed, 15 Jul 2026 05:15:50 +0100 Subject: [PATCH] Complete planar camera lifecycle and lens --- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 62 ++++++++++++++++++- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 14 ++++- .../EveUnitySceneSurfaceLowererTests.cs | 40 +++++++++++- .../org.gamecult.eve.unity-scene/package.json | 2 +- 4 files changed, 111 insertions(+), 7 deletions(-) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index d7134ed..69d717d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -20,6 +20,9 @@ namespace GameCult.Eve.UnityScene [SerializeField] private float yawDegrees = 35f; [SerializeField] private float followDamping = 12f; private IEveUnityCameraRenderPolicySource? _renderPolicySource; + private bool _ownsAmbientEnvironment; + private AmbientMode _previousAmbientMode; + private Color _previousAmbientLight; public EveUnityPlayableWorldClientHost? Host { @@ -39,12 +42,27 @@ namespace GameCult.Eve.UnityScene set => _renderPolicySource = value; } + public void ReleaseRig() => RestoreAmbientEnvironment(); + public bool ApplyRig(float deltaTime) { var resolvedHost = ResolveHost(); var activeWorld = resolvedHost?.ActiveWorld; if (resolvedHost == null || activeWorld == null) + { + RestoreAmbientEnvironment(); return false; + } + + if (activeWorld.CameraRig == "planar.top-down-follow.v1" && !HasValidPlanarContract(activeWorld)) + { + RestoreAmbientEnvironment(); + return false; + } + if (activeWorld.CameraRig == "planar.top-down-follow.v1") + ApplyAmbientEnvironment(activeWorld); + else + RestoreAmbientEnvironment(); if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) && activeWorld.CameraRig != "planar.top-down-follow.v1" && @@ -106,10 +124,16 @@ namespace GameCult.Eve.UnityScene Vector3 target, float deltaTime) { - var resolvedDistance = Mathf.Max(0.1f, world.CameraDistance); - var verticalFov = Mathf.Clamp(world.CameraVerticalFieldOfViewDegrees, 1f, 179f); + var resolvedDistance = world.CameraDistance; + var verticalFov = world.CameraVerticalFieldOfViewDegrees; if (cameraComponent != null) + { + cameraComponent.orthographic = false; + cameraComponent.lensShift = Vector2.zero; cameraComponent.fieldOfView = verticalFov; + cameraComponent.nearClipPlane = world.CameraNearClipPlane; + cameraComponent.farClipPlane = world.CameraFarClipPlane; + } var aspect = cameraComponent != null ? cameraComponent.aspect : 16f / 9f; var verticalHalfExtent = resolvedDistance * Mathf.Tan(verticalFov * 0.5f * Mathf.Deg2Rad); var horizontalHalfExtent = verticalHalfExtent * Mathf.Max(0.01f, aspect); @@ -125,13 +149,41 @@ namespace GameCult.Eve.UnityScene camera.position = Vector3.Lerp(camera.position, desiredPosition, t); camera.rotation = Quaternion.LookRotation(Vector3.down, Vector3.forward); + return true; + } + + private static bool HasValidPlanarContract(EveUnityPlayableWorldProjection world) => + world.CameraDistance > 0f && + world.CameraVerticalFieldOfViewDegrees > 0f && + world.CameraVerticalFieldOfViewDegrees < 180f && + world.CameraTargetScreenX >= 0f && world.CameraTargetScreenX <= 1f && + world.CameraTargetScreenY >= 0f && world.CameraTargetScreenY <= 1f && + world.CameraPositionDamping >= 0f && + world.CameraNearClipPlane > 0f && + world.CameraFarClipPlane > world.CameraNearClipPlane; + + private void ApplyAmbientEnvironment(EveUnityPlayableWorldProjection world) + { + if (!_ownsAmbientEnvironment) + { + _previousAmbientMode = RenderSettings.ambientMode; + _previousAmbientLight = RenderSettings.ambientLight; + _ownsAmbientEnvironment = true; + } RenderSettings.ambientMode = AmbientMode.Flat; RenderSettings.ambientLight = new Color( Mathf.Max(0f, world.AmbientLightR * world.AmbientLightIntensity), Mathf.Max(0f, world.AmbientLightG * world.AmbientLightIntensity), Mathf.Max(0f, world.AmbientLightB * world.AmbientLightIntensity), 1f); - return true; + } + + private void RestoreAmbientEnvironment() + { + if (!_ownsAmbientEnvironment) return; + RenderSettings.ambientMode = _previousAmbientMode; + RenderSettings.ambientLight = _previousAmbientLight; + _ownsAmbientEnvironment = false; } private static Bounds? CalculateVisualBounds(Transform player, int cullingMask) @@ -168,6 +220,10 @@ namespace GameCult.Eve.UnityScene ApplyRig(Time.deltaTime); } + private void OnDisable() => ReleaseRig(); + + private void OnDestroy() => ReleaseRig(); + private EveUnityPlayableWorldClientHost? ResolveHost() { if (host != null) diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 5dd3f2e..b8a33a0 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -134,7 +134,9 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("cameraTargetScreenY"), 0.5f), ParseFloat(worldRoot.GetProp("cameraPositionDamping"), 0f), ParseVector3(worldRoot.GetProp("ambientLightColor")), - ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f)); + ParseFloat(worldRoot.GetProp("ambientLightIntensity"), 1f), + ParseFloat(worldRoot.GetProp("cameraNearClipPlane"), 0f), + ParseFloat(worldRoot.GetProp("cameraFarClipPlane"), 0f)); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -353,7 +355,9 @@ namespace GameCult.Eve.UnityScene float cameraTargetScreenY = 0.5f, float cameraPositionDamping = 0f, (float r, float g, float b) ambientLightColor = default, - float ambientLightIntensity = 1f) + float ambientLightIntensity = 1f, + float cameraNearClipPlane = 0f, + float cameraFarClipPlane = 0f) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -382,6 +386,8 @@ namespace GameCult.Eve.UnityScene AmbientLightG = ambientLightColor.g; AmbientLightB = ambientLightColor.b; AmbientLightIntensity = ambientLightIntensity; + CameraNearClipPlane = cameraNearClipPlane; + CameraFarClipPlane = cameraFarClipPlane; } public string WorldRootId { get; } @@ -414,6 +420,10 @@ namespace GameCult.Eve.UnityScene public float CameraPositionDamping { get; } + public float CameraNearClipPlane { get; } + + public float CameraFarClipPlane { get; } + public float AmbientLightR { get; } public float AmbientLightG { get; } diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs index 53b90cf..c1f7051 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -389,7 +389,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.Root.Children[0].SceneObjectKind, Is.EqualTo("playable-world-root")); Assert.That(projection.Root.Children[0].Children[0].SceneObjectKind, Is.EqualTo("playable-world-entity")); - Assert.That(projection.Root.Children[0].Children[3].SceneObjectKind, Is.EqualTo("world-field-3d")); + Assert.That(projection.Root.Children[0].Children.Any(child => child.SceneObjectKind == "world-field-3d"), Is.True); Assert.That(projection.PlayableWorld, Is.Not.Null); Assert.That(projection.PlayableWorld!.WorldRootId, Is.EqualTo("aetheria.daemon.game.playable")); Assert.That(projection.PlayableWorld.StatePointerId, Is.EqualTo("cultmesh://aetheria/run/current")); @@ -405,6 +405,8 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.CameraTargetScreenX, Is.EqualTo(0.9f)); Assert.That(projection.PlayableWorld.CameraTargetScreenY, Is.EqualTo(0.55f)); Assert.That(projection.PlayableWorld.CameraPositionDamping, Is.EqualTo(5f)); + Assert.That(projection.PlayableWorld.CameraNearClipPlane, Is.EqualTo(0.3f)); + Assert.That(projection.PlayableWorld.CameraFarClipPlane, Is.EqualTo(4096f)); Assert.That(projection.PlayableWorld.AmbientLightR, Is.EqualTo(0.2f)); Assert.That(projection.PlayableWorld.AmbientLightG, Is.EqualTo(0.2f)); Assert.That(projection.PlayableWorld.AmbientLightB, Is.EqualTo(0.2f)); @@ -1057,6 +1059,7 @@ namespace GameCult.Eve.UnityScene.Tests var host = hostObject.AddComponent(); host.Configure(rootObject.transform, provider, provider, provider, provider); host.Connect(); + hostObject.SetActive(true); var rig = hostObject.AddComponent(); rig.Host = host; @@ -1064,6 +1067,8 @@ namespace GameCult.Eve.UnityScene.Tests rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); var camera = cameraObject.AddComponent(); camera.aspect = 16f / 9f; + camera.orthographic = true; + camera.lensShift = new Vector2(0.25f, -0.2f); var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); @@ -1075,6 +1080,10 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(rig.ApplyRig(0f), Is.True); Assert.That(camera.cullingMask & (1 << 14), Is.Zero); Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f)); + Assert.That(camera.orthographic, Is.False); + Assert.That(camera.lensShift, Is.EqualTo(Vector2.zero)); + Assert.That(camera.nearClipPlane, Is.EqualTo(0.3f).Within(0.001f)); + Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f)); Assert.That(cameraObject.transform.position.y - player.transform.position.y, Is.EqualTo(150f).Within(0.001f)); var viewport = camera.WorldToViewportPoint(player.transform.position); Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); @@ -1082,12 +1091,27 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(RenderSettings.ambientMode, Is.EqualTo(UnityEngine.Rendering.AmbientMode.Flat)); Assert.That(RenderSettings.ambientLight.r, Is.EqualTo(0.2f).Within(0.001f)); + var selectedTarget = rootObject.GetComponentsInChildren() + .Single(marker => marker.EntityId == "raider-scout"); + Assert.That(EveUnityCombatPresentation.Find(host.ActiveProjection)?.SelectedTargetEntityId, + Is.EqualTo("raider-scout")); + var poseWithoutSelectedTarget = cameraObject.transform.position; + selectedTarget.transform.position = new Vector3(10000f, 0f, -10000f); + Assert.That(rig.ApplyRig(0f), Is.True); + Assert.That(cameraObject.transform.position, Is.EqualTo(poseWithoutSelectedTarget)); + viewport = camera.WorldToViewportPoint(player.transform.position); + Assert.That(viewport.x, Is.EqualTo(0.9f).Within(0.001f)); + Assert.That(viewport.y, Is.EqualTo(0.55f).Within(0.001f)); + var beforeFollow = cameraObject.transform.position; player.transform.position += Vector3.right * 10f; Assert.That(rig.ApplyRig(0.1f), Is.True); Assert.That( cameraObject.transform.position.x - beforeFollow.x, Is.EqualTo(10f * (1f - Mathf.Exp(-0.5f))).Within(0.001f)); + rig.ReleaseRig(); + Assert.That(RenderSettings.ambientMode, Is.EqualTo(ambientMode)); + Assert.That(RenderSettings.ambientLight, Is.EqualTo(ambientLight)); } finally { @@ -1664,6 +1688,18 @@ namespace GameCult.Eve.UnityScene.Tests "", "")); + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.combat", + "combat.presentation", + new Dictionary(StringComparer.Ordinal) + { + ["controlledEntityId"] = "player-vanguard", + ["selectedTargetEntityId"] = "raider-scout", + ["targetVisible"] = "true", + ["targetHostile"] = "true" + }, + Array.Empty())); + playableChildren.Add(new EveSurfaceComponent( "aetheria.daemon.game.flow", "field.vector3d", @@ -1709,6 +1745,8 @@ namespace GameCult.Eve.UnityScene.Tests ["cameraTargetScreenX"] = "0.9", ["cameraTargetScreenY"] = "0.55", ["cameraPositionDamping"] = "5", + ["cameraNearClipPlane"] = "0.3", + ["cameraFarClipPlane"] = "4096", ["ambientLightColor"] = "0.2,0.2,0.2", ["ambientLightIntensity"] = "1", ["excludedRenderChannels"] = "map", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 3589d71..d6f6274 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.22", + "version": "0.3.23", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {