Lower semantic render channels in Unity

This commit is contained in:
Meta Vens 2026-07-13 21:35:11 +01:00
commit e451719520
8 changed files with 78 additions and 28 deletions

View file

@ -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

View file

@ -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.<channel>.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.

View file

@ -46,7 +46,7 @@ namespace GameCult.Eve.UnityScene
private readonly Dictionary<string, GameObject> _prefabs = new Dictionary<string, GameObject>(StringComparer.Ordinal);
private readonly Dictionary<string, UnityEngine.Object> _nativeAssets = new Dictionary<string, UnityEngine.Object>(StringComparer.Ordinal);
private readonly List<AssetBundle> _assetBundles = new List<AssetBundle>();
private readonly Dictionary<string, int> _cameraCullingMasks = new Dictionary<string, int>(StringComparer.Ordinal);
private readonly Dictionary<string, int> _renderChannelLayers = new Dictionary<string, int>(StringComparer.Ordinal);
private readonly ConcurrentQueue<object> _liveDocuments = new ConcurrentQueue<object>();
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<EveAssetVariant> 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;
}
}
}

View file

@ -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()

View file

@ -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<Camera>();
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);

View file

@ -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<string> 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<string>? excludedRenderChannels = null)
{
WorldRootId = worldRootId ?? "";
StatePointerId = statePointerId ?? "";
@ -342,6 +355,7 @@ namespace GameCult.Eve.UnityScene
EntityViewSchema = entityViewSchema ?? "";
ZoneRenderPointerId = zoneRenderPointerId ?? "";
ZoneRenderSchema = zoneRenderSchema ?? "";
ExcludedRenderChannels = excludedRenderChannels ?? Array.Empty<string>();
}
public string WorldRootId { get; }
@ -374,6 +388,8 @@ namespace GameCult.Eve.UnityScene
public string ActionCommand { get; }
public IReadOnlyList<string> ExcludedRenderChannels { get; }
public int EntityCount => Entities.Count;
public IReadOnlyList<EveUnityPlayableWorldEntity> Entities { get; }

View file

@ -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<EveUnityPlayableWorldCameraRig>();
rig.Host = host;
rig.CameraTransform = cameraObject.transform;
rig.RenderPolicySource = new FixedRenderChannelPolicy("map", 14);
var camera = cameraObject.AddComponent<Camera>();
var player = rootObject.GetComponentInChildren<EveUnityPlayableWorldEntityMarker>();
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,

View file

@ -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": {