From e45171952030dbb7ea3997f7abdc0af96b483b26 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Mon, 13 Jul 2026 21:35:11 +0100 Subject: [PATCH] Lower semantic render channels in Unity --- docs/authority-map.md | 10 ++++--- .../org.gamecult.eve.unity-scene/README.md | 8 ++++++ .../EveUnityCultMeshLiveProviderTransport.cs | 26 ++++++++----------- .../EveUnityCultMeshPlayableWorldProvider.cs | 4 +-- .../Runtime/EveUnityPlayableWorldCameraRig.cs | 13 +++++++--- .../Runtime/EveUnitySceneSurfaceLowerer.cs | 20 ++++++++++++-- .../EveUnitySceneSurfaceLowererTests.cs | 23 ++++++++++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 8 files changed, 78 insertions(+), 28 deletions(-) diff --git a/docs/authority-map.md b/docs/authority-map.md index 15c6148..635cd36 100644 --- a/docs/authority-map.md +++ b/docs/authority-map.md @@ -18,6 +18,7 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. - `gamecult.eve.surface.v1` - provider asset-manifest documents - provider command receipts +- semantic render-channel inclusion and exclusion policy - Unity input and local presentation assets ## Outputs @@ -29,15 +30,16 @@ drivers, asset-provider hooks, package release, runtime tests, and captures. ## Derived State -`ActiveWorld`, scene objects, transforms, markers, camera pose, asset caches, -presentation counters, and receipt display state are projections. They are not -world truth. +`ActiveWorld`, scene objects, transforms, markers, camera pose, native +render-channel layer mappings, asset caches, presentation counters, and receipt +display state are projections. They are not world truth. ## Forbidden Writers Unity transforms, input drivers, camera rigs, scene sinks, asset caches, and pending receipts may not mutate or simulate provider world state. Plugin -projection shells may not implement Sai, Norn, or TeX semantics. +projection shells may not implement Sai, Norn, or TeX semantics. Portable +surfaces may not carry Unity layer numbers or camera culling masks. ## Shared Paths diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 12ca9f7..e94e088 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -21,6 +21,14 @@ id or source index. Camera, selection, and provider UI adapters consume that registry instead of reconstructing provider gameplay objects. It owns presentation lookup only; simulation state and commands remain provider-owned. +World surfaces may exclude semantic render channels through the +`excludedRenderChannels` world-root property. Runtime asset variants map those +channel names to Unity layers with `renderChannel..unityLayer` +metadata. Surfaces never publish Unity layer numbers or camera masks: providers +own which semantic content belongs in a view, while EveUnity owns the native +layer mapping and subtracts only those layers from the camera's configured +culling mask. + Retained provider feedback is lowered without becoming Unity authority. `EveUnityFeedbackPresenter` emits each new `feedback.event` identity once, and `EveUnityShotReceiptPresenter` does the same for `shot.receipt` trajectories. diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs index de75b0c..8f51c57 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -46,7 +46,7 @@ namespace GameCult.Eve.UnityScene private readonly Dictionary _prefabs = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _nativeAssets = new Dictionary(StringComparer.Ordinal); private readonly List _assetBundles = new List(); - private readonly Dictionary _cameraCullingMasks = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); private CultMeshNode? _node; private CultMeshSnapshotSession? _snapshot; @@ -210,9 +210,9 @@ namespace GameCult.Eve.UnityScene ? value : null; } - public bool TryGetCameraCullingMask(string viewId, out int cullingMask) + public bool TryGetRenderChannelLayer(string channel, out int layer) { - return _cameraCullingMasks.TryGetValue(viewId ?? "", out cullingMask); + return _renderChannelLayers.TryGetValue(channel ?? "", out layer); } private void EnsureOpen() @@ -497,7 +497,7 @@ namespace GameCult.Eve.UnityScene _assetBundles.Clear(); _prefabs.Clear(); _nativeAssets.Clear(); - _cameraCullingMasks.Clear(); + _renderChannelLayers.Clear(); var selected = catalog.Assets .Select(asset => new { @@ -547,24 +547,20 @@ namespace GameCult.Eve.UnityScene private void ReadCameraPolicies(IEnumerable variants) { - _cameraCullingMasks.Clear(); + _renderChannelLayers.Clear(); foreach (var variant in variants) { foreach (var pair in variant.Metadata) { - const string prefix = "view."; - const string suffix = ".excludeUnityLayers"; + const string prefix = "renderChannel."; + const string suffix = ".unityLayer"; if (!pair.Key.StartsWith(prefix, StringComparison.Ordinal) || !pair.Key.EndsWith(suffix, StringComparison.Ordinal)) continue; - var viewId = pair.Key.Substring(prefix.Length, pair.Key.Length - prefix.Length - suffix.Length); - var mask = -1; - foreach (var token in (pair.Value ?? "").Split(',')) - { - if (int.TryParse(token.Trim(), out var layer) && layer >= 0 && layer < 32) - mask &= ~(1 << layer); - } - _cameraCullingMasks[viewId] = mask; + var channel = pair.Key.Substring(prefix.Length, pair.Key.Length - prefix.Length - suffix.Length); + if (!string.IsNullOrWhiteSpace(channel) && + int.TryParse(pair.Value, out var layer) && layer >= 0 && layer < 32) + _renderChannelLayers[channel] = layer; } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs index ec1a239..a91c832 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -108,10 +108,10 @@ namespace GameCult.Eve.UnityScene return _transport!.ResolveAsset(asset, assetType); } - public bool TryGetCameraCullingMask(string viewId, out int cullingMask) + public bool TryGetRenderChannelLayer(string channel, out int layer) { EnsureTransport(); - return _transport!.TryGetCameraCullingMask(viewId, out cullingMask); + return _transport!.TryGetRenderChannelLayer(channel, out layer); } private void OnDestroy() diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs index ade131c..b4de75c 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldCameraRig.cs @@ -6,7 +6,7 @@ namespace GameCult.Eve.UnityScene { public interface IEveUnityCameraRenderPolicySource { - bool TryGetCameraCullingMask(string viewId, out int cullingMask); + bool TryGetRenderChannelLayer(string channel, out int layer); } public sealed class EveUnityPlayableWorldCameraRig : MonoBehaviour @@ -58,10 +58,15 @@ namespace GameCult.Eve.UnityScene return false; var cameraComponent = camera.GetComponent(); - if (cameraComponent != null && - _renderPolicySource != null && - _renderPolicySource.TryGetCameraCullingMask(activeWorld.ViewId, out var cullingMask)) + if (cameraComponent != null && _renderPolicySource != null) { + var cullingMask = cameraComponent.cullingMask; + foreach (var channel in activeWorld.ExcludedRenderChannels) + { + if (_renderPolicySource.TryGetRenderChannelLayer(channel, out var layer) && + layer >= 0 && layer < 32) + cullingMask &= ~(1 << layer); + } cameraComponent.cullingMask = cullingMask; } var bounds = CalculateVisualBounds(player); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index ae13226..c6d5c88 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using GameCult.Eve.Surface; using GameCult.Mesh; @@ -124,7 +125,8 @@ namespace GameCult.Eve.UnityScene worldRoot.GetProp("entityViewPointerId"), worldRoot.GetProp("entityViewSchema"), worldRoot.GetProp("zoneRenderPointerId"), - worldRoot.GetProp("zoneRenderSchema")); + worldRoot.GetProp("zoneRenderSchema"), + ParseStringList(worldRoot.GetProp("excludedRenderChannels"))); } private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) @@ -198,6 +200,16 @@ namespace GameCult.Eve.UnityScene string.Equals(value, "1", StringComparison.Ordinal); } + private static IReadOnlyList ParseStringList(string value) + { + return (value ?? "") + .Split(',') + .Select(part => part.Trim()) + .Where(part => !string.IsNullOrWhiteSpace(part)) + .Distinct(StringComparer.Ordinal) + .ToArray(); + } + private static EveUnityScenePluginProjection? BuildPluginProjection(EveSurfaceComponent component) { if (SaiVisualNovelAdapter.CanProject(component)) @@ -324,7 +336,8 @@ namespace GameCult.Eve.UnityScene string entityViewPointerId = "", string entityViewSchema = "", string zoneRenderPointerId = "", - string zoneRenderSchema = "") + string zoneRenderSchema = "", + IReadOnlyList? excludedRenderChannels = null) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -342,6 +355,7 @@ namespace GameCult.Eve.UnityScene EntityViewSchema = entityViewSchema ?? ""; ZoneRenderPointerId = zoneRenderPointerId ?? ""; ZoneRenderSchema = zoneRenderSchema ?? ""; + ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty(); } public string WorldRootId { get; } @@ -374,6 +388,8 @@ namespace GameCult.Eve.UnityScene public string ActionCommand { get; } + public IReadOnlyList ExcludedRenderChannels { get; } + public int EntityCount => Entities.Count; public IReadOnlyList Entities { 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 443fa6d..9d47936 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -332,6 +332,7 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.AssetManifest, Is.EqualTo("cultmesh://aetheria/assets/manifest")); Assert.That(projection.PlayableWorld.InputProfile, Is.EqualTo("arpg-third-person")); Assert.That(projection.PlayableWorld.CameraRig, Is.EqualTo("third-person-orbit")); + Assert.That(projection.PlayableWorld.ExcludedRenderChannels, Is.EqualTo(new[] { "map" })); Assert.That(projection.PlayableWorld.PlayerEntityId, Is.EqualTo("player-vanguard")); Assert.That(projection.PlayableWorld.MovementCommand, Is.EqualTo("aetheria.daemon.move_intent")); Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus")); @@ -939,6 +940,8 @@ namespace GameCult.Eve.UnityScene.Tests var rig = hostObject.AddComponent(); rig.Host = host; rig.CameraTransform = cameraObject.transform; + rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14); + var camera = cameraObject.AddComponent(); var player = rootObject.GetComponentInChildren(); Assert.That(player, Is.Not.Null); @@ -947,6 +950,7 @@ namespace GameCult.Eve.UnityScene.Tests largeVisual.transform.localScale = Vector3.one * 100f; Assert.That(rig.ApplyRig(0f), Is.True); + Assert.That(camera.cullingMask & (1 << 14), Is.Zero); Assert.That(cameraObject.transform.position.y, Is.GreaterThan(0f)); Assert.That(Vector3.Distance(cameraObject.transform.position, largeVisual.transform.position), Is.GreaterThan(100f)); } @@ -1562,6 +1566,7 @@ namespace GameCult.Eve.UnityScene.Tests ["assetManifest"] = "cultmesh://aetheria/assets/manifest", ["inputProfile"] = "arpg-third-person", ["cameraRig"] = "third-person-orbit", + ["excludedRenderChannels"] = "map", ["playerEntityId"] = "player-vanguard", ["movementCommand"] = "aetheria.daemon.move_intent", ["focusCommand"] = "aetheria.daemon.focus", @@ -2120,6 +2125,24 @@ namespace GameCult.Eve.UnityScene.Tests } } + private sealed class FixedRenderChannelPolicy : IEveUnityCameraRenderPolicySource + { + private readonly string _channel; + private readonly int _layer; + + public FixedRenderChannelPolicy(string channel, int layer) + { + _channel = channel; + _layer = layer; + } + + public bool TryGetRenderChannelLayer(string channel, out int layer) + { + layer = _layer; + return string.Equals(channel, _channel, StringComparison.Ordinal); + } + } + private sealed class FakePlayableWorldProviderComponent : MonoBehaviour, IEveUnitySceneProviderSurfaceDocumentSource, diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 334ca56..02b50f3 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.2.4", + "version": "0.3.0", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {