Move volume shader ABI into runtime asset variants

This commit is contained in:
Meta Vens 2026-07-15 19:59:37 +01:00
commit 5ce6548007
9 changed files with 166 additions and 50 deletions

View file

@ -62,3 +62,11 @@ therefore reference the effect without importing Aetheria gameplay code.
`org.gamecult.eve.plugin-fields` contracts. `EveFieldsSplatRasterizer` owns the
Unity structured-buffer and RenderTexture lowering without importing provider
document classes.
`field.volume3d` surfaces name logical texture ports, feature names, quality,
and authored scalar/vector parameters. Concrete Unity shader properties,
keywords, and pass indices belong to the selected `unity-scene` asset variant
under `unity.volume.*` metadata. The lowerer fails closed when that native
program descriptor is absent or incomplete. This keeps provider shader ABIs
out of Eve's semantic surface while allowing a provider-owned shader to consume
portable Eve Fields documents.

View file

@ -48,6 +48,8 @@ namespace GameCult.Eve.UnityScene
private readonly HashSet<string> _publishedReceiptIds = new HashSet<string>(StringComparer.Ordinal);
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 Dictionary<string, IReadOnlyDictionary<string, string>> _nativeAssetMetadata =
new Dictionary<string, IReadOnlyDictionary<string, string>>(StringComparer.Ordinal);
private readonly List<AssetBundle> _assetBundles = new List<AssetBundle>();
private readonly Dictionary<string, int> _renderChannelLayers = new Dictionary<string, int>(StringComparer.Ordinal);
private readonly ConcurrentQueue<object> _liveDocuments = new ConcurrentQueue<object>();
@ -314,6 +316,7 @@ namespace GameCult.Eve.UnityScene
_assetBundles.Clear();
_prefabs.Clear();
_nativeAssets.Clear();
_nativeAssetMetadata.Clear();
_node?.Dispose();
_snapshot?.Dispose();
_subscriptions?.Dispose();
@ -344,6 +347,14 @@ namespace GameCult.Eve.UnityScene
? value : null;
}
public bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata)
{
if (asset == null) throw new ArgumentNullException(nameof(asset));
return _nativeAssetMetadata.TryGetValue(asset.AssetRef, out metadata!);
}
public bool TryGetRenderChannelLayer(string channel, out int layer)
{
return _renderChannelLayers.TryGetValue(channel ?? "", out layer);
@ -679,6 +690,7 @@ namespace GameCult.Eve.UnityScene
_assetBundles.Clear();
_prefabs.Clear();
_nativeAssets.Clear();
_nativeAssetMetadata.Clear();
_renderChannelLayers.Clear();
var selected = catalog.Assets
.Select(asset => new
@ -728,9 +740,14 @@ namespace GameCult.Eve.UnityScene
$"'{selection.Variant.AssetKey}'. Available assets: " +
string.Join(", ", bundleAssetNames));
_nativeAssets[selection.Asset.AssetRef] = value;
var metadata = MergeAssetMetadata(selection.Asset.Metadata, selection.Variant.Metadata);
_nativeAssetMetadata[selection.Asset.AssetRef] = metadata;
if (selection.Asset.Metadata.TryGetValue("presentationRole", out var role) &&
!string.IsNullOrWhiteSpace(role))
{
_nativeAssets[role] = value;
_nativeAssetMetadata[role] = metadata;
}
if (value is GameObject prefab)
{
_prefabs[selection.Asset.AssetRef] = prefab;
@ -748,6 +765,16 @@ namespace GameCult.Eve.UnityScene
string.Equals(assetName, advertisedAssetKey, StringComparison.OrdinalIgnoreCase));
}
private static IReadOnlyDictionary<string, string> MergeAssetMetadata(
IReadOnlyDictionary<string, string> assetMetadata,
IReadOnlyDictionary<string, string> variantMetadata)
{
var merged = new Dictionary<string, string>(StringComparer.Ordinal);
foreach (var pair in assetMetadata) merged[pair.Key] = pair.Value;
foreach (var pair in variantMetadata) merged[pair.Key] = pair.Value;
return merged;
}
private void ReadCameraPolicies(IEnumerable<EveAssetVariant> variants)
{
_renderChannelLayers.Clear();

View file

@ -1,6 +1,7 @@
using System;
using System.IO;
using System.Collections.Concurrent;
using System.Collections.Generic;
using GameCult.Eve.Surface;
using GameCult.Eve.PluginFields;
using GameCult.Eve.UnityScene.Fields;
@ -22,6 +23,7 @@ namespace GameCult.Eve.UnityScene
IEveUnityFieldsSplatsDocumentSource,
IEveUnityCameraRenderPolicySource,
IEveUnityNativeAssetProvider,
IEveUnityNativeAssetMetadataProvider,
IEveUnityInputCapabilitySource
{
[SerializeField] private string rendezvousEndpoint = "";
@ -124,6 +126,14 @@ namespace GameCult.Eve.UnityScene
return _transport!.ResolveAsset(asset, assetType);
}
public bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata)
{
EnsureTransport();
return _transport!.TryResolveAssetMetadata(asset, out metadata);
}
public bool TryGetRenderChannelLayer(string channel, out int layer)
{
EnsureTransport();

View file

@ -17,6 +17,13 @@ namespace GameCult.Eve.UnityScene
UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType);
}
public interface IEveUnityNativeAssetMetadataProvider
{
bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata);
}
public sealed class EveUnityGameObjectPlayableWorldSceneSink :
IEveUnityPlayableWorldSceneSink,
IEveUnityEntityGenerationSink,

View file

@ -234,7 +234,9 @@ namespace GameCult.Eve.UnityScene
_entityPresenter.Apply(document, lease);
}
public sealed class EveUnityLivePlayableWorldAssetProvider : IEveUnityNativeAssetProvider
public sealed class EveUnityLivePlayableWorldAssetProvider :
IEveUnityNativeAssetProvider,
IEveUnityNativeAssetMetadataProvider
{
private readonly EveUnityPlayableWorldAssetManifestCache _assetManifests;
private readonly Func<EveUnityPlayableWorldProjection?> _activeWorld;
@ -281,5 +283,16 @@ namespace GameCult.Eve.UnityScene
}
return (_fallback as IEveUnityNativeAssetProvider)?.ResolveAsset(asset, assetType);
}
public bool TryResolveAssetMetadata(
EveUnityPlayableWorldAssetBinding asset,
out IReadOnlyDictionary<string, string> metadata)
{
if (asset == null) throw new ArgumentNullException(nameof(asset));
if (_fallback is IEveUnityNativeAssetMetadataProvider provider)
return provider.TryResolveAssetMetadata(asset, out metadata);
metadata = new Dictionary<string, string>(StringComparer.Ordinal);
return false;
}
}
}

View file

@ -19,6 +19,7 @@ namespace GameCult.Eve.UnityScene.Fields
private readonly List<EveFieldsSplatLayerTarget> _targets = new List<EveFieldsSplatLayerTarget>();
private Material? _material;
private Shader? _sourceShader;
private IReadOnlyDictionary<string, string>? _programMetadata;
private RenderTexture? _cloudTexture;
private CommandBuffer? _commands;
private string _activeNodeId = "";
@ -87,25 +88,13 @@ namespace GameCult.Eve.UnityScene.Fields
if (_cloudTexture == null) return;
var gpuProjection = GL.GetGPUProjectionMatrix(camera.projectionMatrix, true);
SetMatrix(field, "cameraInverseViewProjectionProperty", (gpuProjection * camera.worldToCameraMatrix).inverse);
SetVector(field, "cameraProjectionExtentsProperty", ProjectionExtents(camera));
SetFloat(field, "raymarchOffsetProperty", Halton(Time.frameCount & 1023, 3));
foreach (var binding in ParseBindings(Prop(field, "screenTextureScaleBindings")))
{
var texture = _material!.GetTexture(binding.Key);
if (texture != null)
_material.SetVector(binding.Value, new Vector4(
(float)Math.Max(1, camera.pixelWidth) / Math.Max(1, texture.width),
(float)Math.Max(1, camera.pixelHeight) / Math.Max(1, texture.height),
0f,
0f));
}
SetMatrixPort("cameraInverseViewProjection", (gpuProjection * camera.worldToCameraMatrix).inverse);
SetVectorPort("cameraProjectionExtents", ProjectionExtents(camera));
SetFloatPort("raymarchOffset", Halton(Time.frameCount & 1023, 3));
var raymarchPass = NonNegativeInt(field, "raymarchPass", 0);
var compositePass = NonNegativeInt(field, "compositePass", 2);
var cloudTextureProperty = Prop(field, "cloudTextureProperty");
if (!string.IsNullOrWhiteSpace(cloudTextureProperty))
_material!.SetTexture(cloudTextureProperty, _cloudTexture);
var raymarchPass = ProgramPass("raymarch");
var compositePass = ProgramPass("composite");
SetTexturePort("cloud", _cloudTexture);
_commands ??= new CommandBuffer { name = "Eve Fields Volume" };
_commands.Clear();
@ -132,30 +121,49 @@ namespace GameCult.Eve.UnityScene.Fields
new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"),
typeof(Shader)) as Shader;
if (shader == null || !shader.isSupported) return false;
var shaderBinding = new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref");
if (provider is not IEveUnityNativeAssetMetadataProvider metadataProvider ||
!metadataProvider.TryResolveAssetMetadata(shaderBinding, out var metadata))
return false;
_programMetadata = metadata;
if (ProgramPass("raymarch") < 0 || ProgramPass("composite") < 0 ||
string.IsNullOrWhiteSpace(ProgramPort("texture", "cloud")) ||
string.IsNullOrWhiteSpace(ProgramPort("matrix", "cameraInverseViewProjection")) ||
string.IsNullOrWhiteSpace(ProgramPort("vector", "cameraProjectionExtents")) ||
string.IsNullOrWhiteSpace(ProgramPort("vector", "viewportTransform")) ||
string.IsNullOrWhiteSpace(ProgramPort("float", "raymarchOffset")))
return false;
if (_material != null && ReferenceEquals(shader, _sourceShader) &&
string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal)) return true;
if (_material != null) Destroy(_material);
_sourceShader = shader;
_activeNodeId = field.NodeId;
_material = new Material(shader) { hideFlags = HideFlags.DontSave };
foreach (var prop in field.Props)
foreach (var parameter in ParseBindings(Prop(field, "floatParameters")))
{
if (prop.Key.StartsWith("materialFloat.", StringComparison.Ordinal) &&
TryFloat(prop.Value, out var value))
_material.SetFloat(prop.Key.Substring("materialFloat.".Length), value);
else if (prop.Key.StartsWith("materialVector.", StringComparison.Ordinal) &&
TryVector(prop.Value, out var vector))
_material.SetVector(prop.Key.Substring("materialVector.".Length), vector);
if (TryFloat(parameter.Value, out var value)) SetFloatPort(parameter.Key, value);
}
foreach (var parameter in ParseBindings(Prop(field, "vectorParameters")))
if (TryVector(parameter.Value, out var vector)) SetVectorPort(parameter.Key, vector);
foreach (var binding in ParseBindings(Prop(field, "assetTextureBindings")))
{
var texture = provider.ResolveAsset(
new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"),
typeof(Texture)) as Texture;
if (texture != null) _material.SetTexture(binding.Value, texture);
if (texture != null) SetTexturePort(binding.Value, texture);
}
var quality = Prop(field, "quality").Trim().ToLowerInvariant();
if (TryProgramValue($"unity.volume.quality.{quality}.keyword", out var keyword) &&
!string.IsNullOrWhiteSpace(keyword))
_material.EnableKeyword(keyword);
foreach (var feature in Prop(field, "features").Split(';'))
{
var logicalFeature = feature.Trim();
if (!string.IsNullOrWhiteSpace(logicalFeature) &&
TryProgramValue($"unity.volume.feature.{logicalFeature}.keyword", out var featureKeyword) &&
!string.IsNullOrWhiteSpace(featureKeyword))
_material.EnableKeyword(featureKeyword);
}
foreach (var keyword in Prop(field, "shaderKeywords").Split(';'))
if (!string.IsNullOrWhiteSpace(keyword)) _material.EnableKeyword(keyword.Trim());
return true;
}
@ -166,20 +174,16 @@ namespace GameCult.Eve.UnityScene.Fields
foreach (var binding in bindings)
{
var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal));
if (target?.TargetTexture != null) _material.SetTexture(binding.Value, target.TargetTexture);
}
var viewportProperty = Prop(field, "viewportTransformProperty");
if (!string.IsNullOrWhiteSpace(viewportProperty))
{
var viewport = document.Viewport;
var width = viewport.MaxX - viewport.MinX;
var height = viewport.MaxY - viewport.MinY;
_material.SetVector(viewportProperty, new Vector4(
(float)((viewport.MinX + viewport.MaxX) * 0.5),
(float)((viewport.MinY + viewport.MaxY) * 0.5),
(float)width,
(float)height));
if (target?.TargetTexture != null) SetTexturePort(binding.Value, target.TargetTexture);
}
var viewport = document.Viewport;
var width = viewport.MaxX - viewport.MinX;
var height = viewport.MaxY - viewport.MinY;
SetVectorPort("viewportTransform", new Vector4(
(float)((viewport.MinX + viewport.MaxX) * 0.5),
(float)((viewport.MinY + viewport.MaxY) * 0.5),
(float)width,
(float)height));
}
private void EnsureTargets(EveUnityFieldVolumeProjection field)
@ -221,29 +225,51 @@ namespace GameCult.Eve.UnityScene.Fields
if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); _cloudTexture = null; }
if (_material != null) { Destroy(_material); _material = null; }
_sourceShader = null;
_programMetadata = null;
_activeNodeId = "";
PresentedFrameId = -1;
CompositeCount = 0;
}
private void SetMatrix(EveUnityFieldVolumeProjection field, string propKey, Matrix4x4 value)
private void SetMatrixPort(string port, Matrix4x4 value)
{
var property = Prop(field, propKey);
var property = ProgramPort("matrix", port);
if (!string.IsNullOrWhiteSpace(property)) _material!.SetMatrix(property, value);
}
private void SetVector(EveUnityFieldVolumeProjection field, string propKey, Vector4 value)
private void SetVectorPort(string port, Vector4 value)
{
var property = Prop(field, propKey);
var property = ProgramPort("vector", port);
if (!string.IsNullOrWhiteSpace(property)) _material!.SetVector(property, value);
}
private void SetFloat(EveUnityFieldVolumeProjection field, string propKey, float value)
private void SetFloatPort(string port, float value)
{
var property = Prop(field, propKey);
var property = ProgramPort("float", port);
if (!string.IsNullOrWhiteSpace(property)) _material!.SetFloat(property, value);
}
private void SetTexturePort(string port, Texture value)
{
var property = ProgramPort("texture", port);
if (!string.IsNullOrWhiteSpace(property)) _material!.SetTexture(property, value);
}
private string ProgramPort(string kind, string port) =>
TryProgramValue($"unity.volume.{kind}Port.{port}", out var value) ? value : "";
private int ProgramPass(string pass) =>
TryProgramValue($"unity.volume.pass.{pass}", out var value) &&
int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var parsed) && parsed >= 0
? parsed
: -1;
private bool TryProgramValue(string key, out string value)
{
value = "";
return _programMetadata != null && _programMetadata.TryGetValue(key, out value!);
}
private static Vector4 ProjectionExtents(Camera camera)
{
var y = camera.orthographic ? camera.orthographicSize : Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView);

View file

@ -1,4 +1,5 @@
using System.Reflection;
using System.Collections.Generic;
using GameCult.Eve.UnityScene;
using NUnit.Framework;
@ -38,5 +39,29 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(result, Is.Null);
}
[Test]
public void SelectedRuntimeVariantOwnsConcreteNativeProgramBindings()
{
var method = typeof(EveUnityCultMeshLiveProviderTransport).GetMethod(
"MergeAssetMetadata",
BindingFlags.Static | BindingFlags.NonPublic);
Assert.That(method, Is.Not.Null);
var result = method!.Invoke(null, new object[]
{
new Dictionary<string, string> { ["presentationRole"] = "environment.volume" },
new Dictionary<string, string>
{
["unity.volume.texturePort.surfaceHeight"] = "_NebulaSurfaceHeight",
["unity.volume.pass.raymarch"] = "0"
}
}) as IReadOnlyDictionary<string, string>;
Assert.That(result, Is.Not.Null);
Assert.That(result!["presentationRole"], Is.EqualTo("environment.volume"));
Assert.That(result["unity.volume.texturePort.surfaceHeight"], Is.EqualTo("_NebulaSurfaceHeight"));
Assert.That(result["unity.volume.pass.raymarch"], Is.EqualTo("0"));
}
}
}

View file

@ -1880,7 +1880,7 @@ namespace GameCult.Eve.UnityScene.Tests
["renderChannel"] = "world.transparent",
["compositeMode"] = "premultiplied-alpha",
["quality"] = "normal",
["layerBindings"] = "fog.surface_height=_NebulaSurfaceHeight;fog.tint=_NebulaTint"
["layerBindings"] = "fog.surface_height=surfaceHeight;fog.tint=tint"
},
Array.Empty<EveSurfaceComponent>()));

View file

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