Move volume shader ABI into runtime asset variants
This commit is contained in:
parent
126075697f
commit
5ce6548007
9 changed files with 166 additions and 50 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>()));
|
||||
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Reference in a new issue