Lower entity-forward follow cameras generically

This commit is contained in:
Meta Vens 2026-07-15 21:30:50 +01:00
commit 35ff288aab
4 changed files with 133 additions and 11 deletions

View file

@ -51,6 +51,14 @@ temporal pass must also advertise its current-sample, history,
previous-view-projection, and reset-history ports; partial temporal programs fail
closed.
The provider selects camera semantics and framing from its authoritative mode.
`planar.top-down-follow.v1` derives a downward presentation from the advertised
target and framing values. `perspective.entity-forward-follow.v1` derives its
view direction from the presented entity rotation and applies the same generic
distance, screen-position, damping, and lens contract. EveUnity owns only the
native camera transform. It cannot collapse distinct provider modes into one
camera opinion or infer a product camera from entity kind.
## Shared Paths
Initial connect discovers a Verse endpoint and selects a provider surface by

View file

@ -80,7 +80,9 @@ namespace GameCult.Eve.UnityScene
return false;
}
if (activeWorld.CameraRig == "planar.top-down-follow.v1" && !HasValidPlanarContract(activeWorld))
if ((activeWorld.CameraRig == "planar.top-down-follow.v1" ||
activeWorld.CameraRig == "perspective.entity-forward-follow.v1") &&
!HasValidFramedFollowContract(activeWorld))
{
ReleaseCamera();
RestoreAmbientEnvironment();
@ -88,6 +90,7 @@ namespace GameCult.Eve.UnityScene
}
if (!string.IsNullOrWhiteSpace(activeWorld.CameraRig) &&
activeWorld.CameraRig != "planar.top-down-follow.v1" &&
activeWorld.CameraRig != "perspective.entity-forward-follow.v1" &&
activeWorld.CameraRig != "arpg.orbital-follow.v1" &&
activeWorld.CameraRig != "third-person-orbit")
{
@ -139,6 +142,8 @@ namespace GameCult.Eve.UnityScene
}
if (activeWorld.CameraRig == "planar.top-down-follow.v1")
return ApplyPlanarTopDown(activeWorld, camera, cameraComponent, player.position, deltaTime);
if (activeWorld.CameraRig == "perspective.entity-forward-follow.v1")
return ApplyEntityForwardFollow(activeWorld, camera, cameraComponent, player, deltaTime);
var bounds = CalculateVisualBounds(
player,
@ -197,7 +202,50 @@ namespace GameCult.Eve.UnityScene
return true;
}
private static bool HasValidPlanarContract(EveUnityPlayableWorldProjection world) =>
private static bool ApplyEntityForwardFollow(
EveUnityPlayableWorldProjection world,
Transform camera,
Camera? cameraComponent,
Transform target,
float deltaTime)
{
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 forward = target.forward;
forward.y = 0f;
if (forward.sqrMagnitude <= 0.0001f)
forward = Vector3.forward;
forward.Normalize();
var rotation = Quaternion.LookRotation(forward, Vector3.up);
var right = rotation * Vector3.right;
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);
var horizontalOffset = (Mathf.Clamp01(world.CameraTargetScreenX) - 0.5f) * 2f * horizontalHalfExtent;
var verticalOffset = (Mathf.Clamp01(world.CameraTargetScreenY) - 0.5f) * 2f * verticalHalfExtent;
var desiredPosition = target.position -
(forward * resolvedDistance) -
(right * horizontalOffset) -
(Vector3.up * verticalOffset);
var damping = Mathf.Max(0f, world.CameraPositionDamping);
var t = deltaTime <= 0f || damping <= 0f
? 1f
: 1f - Mathf.Exp(-damping * deltaTime);
camera.position = Vector3.Lerp(camera.position, desiredPosition, t);
camera.rotation = rotation;
return true;
}
private static bool HasValidFramedFollowContract(EveUnityPlayableWorldProjection world) =>
world.CameraDistance > 0f &&
world.CameraVerticalFieldOfViewDegrees > 0f &&
world.CameraVerticalFieldOfViewDegrees < 180f &&

View file

@ -1265,6 +1265,65 @@ namespace GameCult.Eve.UnityScene.Tests
}
}
[Test]
public void PlayableWorldCameraRigFramesEntityForwardPerspectiveWithoutProviderTypes()
{
var rootObject = new GameObject("generic-forward-world-root");
var hostObject = new GameObject("generic-forward-client");
var cameraObject = new GameObject("generic-forward-camera");
hostObject.SetActive(false);
try
{
var provider = hostObject.AddComponent<FakePlayableWorldProviderComponent>();
var nativeAssets = hostObject.AddComponent<FixedNativeAssetProviderComponent>();
provider.Set(
new EveUnitySceneProviderSurfaceDocument(
PlayableArpgDocument(
cameraRig: "perspective.entity-forward-follow.v1",
cameraDistance: "30",
cameraTargetScreenX: "0.64",
cameraTargetScreenY: "0.81",
cameraPositionDamping: "0",
cameraNearClipPlane: "1",
cameraFarClipPlane: "4096"),
Advertisement("aetheria.daemon.game"),
"cultmesh://generic/eve/surfaces/forward",
1),
new EveUnityPlayableWorldAssetManifestDocument(
"cultmesh://generic/assets/manifest",
Array.Empty<EveUnityPlayableWorldAssetManifestDocumentEntry>(),
"generic"));
var host = hostObject.AddComponent<EveUnityPlayableWorldClientHost>();
host.Configure(rootObject.transform, provider, provider, provider, provider, nativeAssets);
host.Connect();
hostObject.SetActive(true);
var camera = cameraObject.AddComponent<Camera>();
camera.aspect = 16f / 9f;
var rig = hostObject.AddComponent<EveUnityPlayableWorldCameraRig>();
rig.Host = host;
rig.CameraTransform = cameraObject.transform;
var player = rootObject.GetComponentInChildren<EveUnityPlayableWorldEntityMarker>();
Assert.That(player, Is.Not.Null);
Assert.That(rig.ApplyRig(0f), Is.True);
Assert.That(Vector3.Dot(cameraObject.transform.forward, player!.transform.forward), Is.GreaterThan(0.999f));
Assert.That(camera.fieldOfView, Is.EqualTo(60f).Within(0.001f));
Assert.That(camera.nearClipPlane, Is.EqualTo(1f).Within(0.001f));
Assert.That(camera.farClipPlane, Is.EqualTo(4096f).Within(0.001f));
var viewport = camera.WorldToViewportPoint(player.transform.position);
Assert.That(viewport.x, Is.EqualTo(0.64f).Within(0.001f));
Assert.That(viewport.y, Is.EqualTo(0.81f).Within(0.001f));
Assert.That(cameraObject.transform.position.y, Is.LessThan(player.transform.position.y));
}
finally
{
UnityEngine.Object.DestroyImmediate(cameraObject);
UnityEngine.Object.DestroyImmediate(hostObject);
UnityEngine.Object.DestroyImmediate(rootObject);
}
}
[Test]
public void ProviderPrefabKeepsAuthoredScaleWhileFallbackUsesSemanticRadius()
{
@ -1773,7 +1832,14 @@ namespace GameCult.Eve.UnityScene.Tests
bool includeRaider = true,
string playerPosition = "0,0,0",
string skyboxAssetRef = "",
string reflectionAssetRef = "")
string reflectionAssetRef = "",
string cameraRig = "planar.top-down-follow.v1",
string cameraDistance = "150",
string cameraTargetScreenX = "0.9",
string cameraTargetScreenY = "0.55",
string cameraPositionDamping = "5",
string cameraNearClipPlane = "0.3",
string cameraFarClipPlane = "4096")
{
var playableChildren = new List<EveSurfaceComponent>
{
@ -1910,15 +1976,15 @@ namespace GameCult.Eve.UnityScene.Tests
["zoneRenderSchema"] = "gamecult.aetheria.zone_render.v1",
["assetManifest"] = "cultmesh://aetheria/assets/manifest",
["inputProfile"] = "arpg-third-person",
["cameraRig"] = "planar.top-down-follow.v1",
["cameraRig"] = cameraRig,
["cameraTargetEntityId"] = "player-vanguard",
["cameraDistance"] = "150",
["cameraDistance"] = cameraDistance,
["cameraVerticalFieldOfViewDegrees"] = "60",
["cameraTargetScreenX"] = "0.9",
["cameraTargetScreenY"] = "0.55",
["cameraPositionDamping"] = "5",
["cameraNearClipPlane"] = "0.3",
["cameraFarClipPlane"] = "4096",
["cameraTargetScreenX"] = cameraTargetScreenX,
["cameraTargetScreenY"] = cameraTargetScreenY,
["cameraPositionDamping"] = cameraPositionDamping,
["cameraNearClipPlane"] = cameraNearClipPlane,
["cameraFarClipPlane"] = cameraFarClipPlane,
["ambientLightColor"] = "0.2,0.2,0.2",
["ambientLightIntensity"] = "1.46",
["skyboxAssetRef"] = skyboxAssetRef,

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.38",
"version": "0.3.39",
"unity": "2021.3",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {