diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index dd93898..f7d3d74 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -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. 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 f90d0ed..21c535b 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -48,6 +48,8 @@ namespace GameCult.Eve.UnityScene private readonly HashSet _publishedReceiptIds = new HashSet(StringComparer.Ordinal); private readonly Dictionary _prefabs = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _nativeAssets = new Dictionary(StringComparer.Ordinal); + private readonly Dictionary> _nativeAssetMetadata = + new Dictionary>(StringComparer.Ordinal); private readonly List _assetBundles = new List(); private readonly Dictionary _renderChannelLayers = new Dictionary(StringComparer.Ordinal); private readonly ConcurrentQueue _liveDocuments = new ConcurrentQueue(); @@ -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 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 MergeAssetMetadata( + IReadOnlyDictionary assetMetadata, + IReadOnlyDictionary variantMetadata) + { + var merged = new Dictionary(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 variants) { _renderChannelLayers.Clear(); 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 fe07bb7..6596fb7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -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 metadata) + { + EnsureTransport(); + return _transport!.TryResolveAssetMetadata(asset, out metadata); + } + public bool TryGetRenderChannelLayer(string channel, out int layer) { EnsureTransport(); diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs index 1fafbe0..e1b5bf3 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityGameObjectPlayableWorldSceneSink.cs @@ -17,6 +17,13 @@ namespace GameCult.Eve.UnityScene UnityEngine.Object? ResolveAsset(EveUnityPlayableWorldAssetBinding asset, Type assetType); } + public interface IEveUnityNativeAssetMetadataProvider + { + bool TryResolveAssetMetadata( + EveUnityPlayableWorldAssetBinding asset, + out IReadOnlyDictionary metadata); + } + public sealed class EveUnityGameObjectPlayableWorldSceneSink : IEveUnityPlayableWorldSceneSink, IEveUnityEntityGenerationSink, diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs index 7af275f..36b034a 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldRuntime.cs @@ -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 _activeWorld; @@ -281,5 +283,16 @@ namespace GameCult.Eve.UnityScene } return (_fallback as IEveUnityNativeAssetProvider)?.ResolveAsset(asset, assetType); } + + public bool TryResolveAssetMetadata( + EveUnityPlayableWorldAssetBinding asset, + out IReadOnlyDictionary metadata) + { + if (asset == null) throw new ArgumentNullException(nameof(asset)); + if (_fallback is IEveUnityNativeAssetMetadataProvider provider) + return provider.TryResolveAssetMetadata(asset, out metadata); + metadata = new Dictionary(StringComparer.Ordinal); + return false; + } } } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 8f97a46..37aeb7f 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -19,6 +19,7 @@ namespace GameCult.Eve.UnityScene.Fields private readonly List _targets = new List(); private Material? _material; private Shader? _sourceShader; + private IReadOnlyDictionary? _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); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs index 17603f9..1fd2603 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnityCultMeshAssetLookupTests.cs @@ -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 { ["presentationRole"] = "environment.volume" }, + new Dictionary + { + ["unity.volume.texturePort.surfaceHeight"] = "_NebulaSurfaceHeight", + ["unity.volume.pass.raymarch"] = "0" + } + }) as IReadOnlyDictionary; + + 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")); + } } } 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 6bc1684..1ab2b4a 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -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())); diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 6efc9a7..a70e5fd 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.36", + "version": "0.3.37", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {