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 3c43f78..f90d0ed 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshLiveProviderTransport.cs @@ -5,7 +5,9 @@ using System.Linq; using System.Collections.Concurrent; using GameCult.Caching; using GameCult.Caching.MessagePack; +using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; +using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using GameCult.Networking; using UnityEngine; @@ -30,6 +32,7 @@ namespace GameCult.Eve.UnityScene typeof(EveCommandReceiptDocument), typeof(EveAssetCatalogDocument), typeof(EveEntitySoaViewDocument), + typeof(EveFieldsSplatsDocument), typeof(CultMeshBodyPublicationDocument), typeof(CultMeshNetworkBodyDocument), typeof(CultMeshCdnArtifactManifest), @@ -60,8 +63,10 @@ namespace GameCult.Eve.UnityScene private CultMeshBodyPublicationResolver? _bodyResolver; private DateTime _nextReceiptPollUtc; private DateTime _nextEntityViewPollUtc; + private DateTime _nextFieldsPollUtc; private long _lastQueuedEntityViewEpoch = -1; private long _lastQueuedEntityViewSequence = -1; + private readonly Dictionary _lastFieldsFrameByRecord = new Dictionary(StringComparer.Ordinal); public EveUnityCultMeshLiveProviderTransport( string replicaPath, @@ -110,6 +115,8 @@ namespace GameCult.Eve.UnityScene public event Action? EntityViewAvailable; + public event Action? FieldsSplatsAvailable; + private static CultMeshBodyPublicationHandle BodyPublicationHandle(EveEntitySoaViewDocument document) { if (document == null) throw new ArgumentNullException(nameof(document)); @@ -174,10 +181,13 @@ namespace GameCult.Eve.UnityScene public void PumpLiveEvents() { PollCurrentEntityView(); + PollCurrentFields(); while (_liveDocuments.TryDequeue(out var document)) { if (document is EveEntitySoaViewDocument entityView) PublishEntityView(entityView); + else if (document is EveFieldsSplatsDocument fields) + FieldsSplatsAvailable?.Invoke(fields); else if (document is EveSurfaceDocument surface) PublishSurface(surface); else if (document is EveCommandReceiptDocument receipt) @@ -186,6 +196,34 @@ namespace GameCult.Eve.UnityScene PollPendingReceipts(); } + private void PollCurrentFields() + { + if (DateTime.UtcNow < _nextFieldsPollUtc) + return; + _nextFieldsPollUtc = DateTime.UtcNow.AddMilliseconds(250); + var world = new EveUnitySceneSurfaceLowerer() + .Lower(CurrentSurfaceDocument.SurfaceDocument, CurrentSurfaceDocument.AdvertisedSurface) + .PlayableWorld; + if (world == null) + return; + foreach (var recordRef in world.FieldVolumes + .Select(field => field.DocumentRef) + .Where(value => !string.IsNullOrWhiteSpace(value)) + .Distinct(StringComparer.Ordinal)) + { + var current = _snapshot! + .FetchDocumentsAsync( + recordKeys: new[] { recordRef }, + schemaIds: new[] { EveFieldsSchemas.Splats }) + .GetAwaiter().GetResult().FirstOrDefault(); + if (current == null || + (_lastFieldsFrameByRecord.TryGetValue(recordRef, out var frame) && current.FrameId <= frame)) + continue; + _lastFieldsFrameByRecord[recordRef] = current.FrameId; + _liveDocuments.Enqueue(current); + } + } + private void PollCurrentEntityView() { if (DateTime.UtcNow < _nextEntityViewPollUtc) @@ -289,6 +327,7 @@ namespace GameCult.Eve.UnityScene _replicaShard = null; _pendingCommandIds.Clear(); _publishedReceiptIds.Clear(); + _lastFieldsFrameByRecord.Clear(); } public GameObject? ResolvePrefab(EveUnityPlayableWorldAssetBinding asset) 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 a2f6ea3..fe07bb7 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/EveUnityCultMeshPlayableWorldProvider.cs @@ -2,6 +2,8 @@ using System; using System.IO; using System.Collections.Concurrent; using GameCult.Eve.Surface; +using GameCult.Eve.PluginFields; +using GameCult.Eve.UnityScene.Fields; using GameCult.Mesh; using UnityEngine; @@ -17,6 +19,7 @@ namespace GameCult.Eve.UnityScene IEveUnitySceneCommandReceiptSource, IEveUnityProviderRefreshSource, IEveUnityEntitySoaViewDocumentSource, + IEveUnityFieldsSplatsDocumentSource, IEveUnityCameraRenderPolicySource, IEveUnityNativeAssetProvider, IEveUnityInputCapabilitySource @@ -32,9 +35,12 @@ namespace GameCult.Eve.UnityScene private EveUnityCultMeshLiveProviderTransport? _transport; private EveUnitySceneLiveProviderBridge? _bridge; private readonly ConcurrentQueue _pendingEntityViews = new ConcurrentQueue(); + private readonly ConcurrentQueue _pendingFields = new ConcurrentQueue(); public event Action? EntityViewAvailable; + public event Action? FieldsSplatsAvailable; + public EveUnityCultMeshProviderSelection? Selection { get; private set; } public string SinkKind => Bridge.SinkKind; @@ -129,6 +135,7 @@ namespace GameCult.Eve.UnityScene _bridge?.Dispose(); _transport?.Dispose(); while (_pendingEntityViews.TryDequeue(out var pending)) pending.Lease.Dispose(); + while (_pendingFields.TryDequeue(out _)) { } _bridge = null; _transport = null; Selection = null; @@ -137,6 +144,11 @@ namespace GameCult.Eve.UnityScene private void Update() { _transport?.PumpLiveEvents(); + EveFieldsSplatsDocument? latestFields = null; + while (_pendingFields.TryDequeue(out var fields)) + latestFields = fields; + if (latestFields != null) + FieldsSplatsAvailable?.Invoke(latestFields); EntityViewLease? latest = null; while (_pendingEntityViews.TryDequeue(out var next)) { @@ -181,6 +193,7 @@ namespace GameCult.Eve.UnityScene Selection.SurfaceId, runtimeId); _transport.EntityViewAvailable += (view, lease) => _pendingEntityViews.Enqueue(new EntityViewLease(view, lease)); + _transport.FieldsSplatsAvailable += fields => _pendingFields.Enqueue(fields); _bridge = new EveUnitySceneLiveProviderBridge(_transport); } diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef index 405df3f..72530cb 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef +++ b/packages/org.gamecult.eve.unity-scene/Runtime/CultMesh/GameCult.Eve.UnityScene.CultMesh.asmdef @@ -4,6 +4,7 @@ "references": [ "GameCult.Eve.Surface", "GameCult.Eve.UnityScene", + "GameCult.Eve.PluginFields", "UnityEngine.AssetBundleModule" ], "includePlatforms": [], diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs index 4dfdf43..f1e0ee2 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityPlayableWorldClientHost.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using GameCult.Eve.Surface; +using GameCult.Eve.UnityScene.Fields; using UnityEngine; #nullable enable @@ -111,6 +112,9 @@ namespace GameCult.Eve.UnityScene if (thermalHud == null) thermalHud = gameObject.AddComponent(); if (thermal == null) thermal = gameObject.AddComponent(); thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider); + var fieldVolume = GetComponent(); + if (fieldVolume == null) fieldVolume = gameObject.AddComponent(); + fieldVolume.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource); var presentation = Runtime.Connect(); ConnectionEpoch++; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs index 5607764..3482d66 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnitySceneSurfaceLowerer.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; using GameCult.Mesh; @@ -103,10 +104,13 @@ namespace GameCult.Eve.UnityScene return null; var entities = new List(); + var fieldVolumes = new List(); foreach (var component in Flatten(worldRoot)) { if (string.Equals(component.Kind, "world.entity3d", StringComparison.Ordinal)) entities.Add(BuildPlayableEntity(component)); + else if (string.Equals(component.Kind, "field.volume3d", StringComparison.Ordinal)) + fieldVolumes.Add(BuildFieldVolume(component)); } return new EveUnityPlayableWorldProjection( @@ -145,9 +149,21 @@ namespace GameCult.Eve.UnityScene ParseFloat(worldRoot.GetProp("reflectionIntensity"), 1f), ParseVector3(worldRoot.GetProp("keyLightDirection")), ParseVector3(worldRoot.GetProp("keyLightColor")), - ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f)); + ParseFloat(worldRoot.GetProp("keyLightIntensity"), 0f), + fieldVolumes); } + private static EveUnityFieldVolumeProjection BuildFieldVolume(EveSurfaceComponent component) => + new EveUnityFieldVolumeProjection( + component.Id, + component.GetProp("documentRef"), + component.GetProp("documentSchema", EveFieldsSchemas.Splats), + component.GetProp("materialAssetRef"), + component.GetProp("renderChannel", "world.transparent"), + component.GetProp("compositeMode", "premultiplied-alpha"), + component.GetProp("quality", "normal"), + component.Props); + private static EveUnityPlayableWorldEntity BuildPlayableEntity(EveSurfaceComponent component) { var position = ParseVector3(component.GetProp("position")); @@ -375,7 +391,8 @@ namespace GameCult.Eve.UnityScene float reflectionIntensity = 1f, (float x, float y, float z) keyLightDirection = default, (float r, float g, float b) keyLightColor = default, - float keyLightIntensity = 0f) + float keyLightIntensity = 0f, + IReadOnlyList? fieldVolumes = null) { WorldRootId = worldRootId ?? ""; StatePointerId = statePointerId ?? ""; @@ -419,6 +436,7 @@ namespace GameCult.Eve.UnityScene KeyLightColorG = keyLightColor.g; KeyLightColorB = keyLightColor.b; KeyLightIntensity = keyLightIntensity; + FieldVolumes = fieldVolumes ?? Array.Empty(); } public string WorldRootId { get; } @@ -506,6 +524,40 @@ namespace GameCult.Eve.UnityScene public int EntityCount => Entities.Count; public IReadOnlyList Entities { get; } + + public IReadOnlyList FieldVolumes { get; } + } + + public sealed class EveUnityFieldVolumeProjection + { + public EveUnityFieldVolumeProjection( + string nodeId, + string documentRef, + string documentSchema, + string materialAssetRef, + string renderChannel, + string compositeMode, + string quality, + IReadOnlyDictionary? props = null) + { + NodeId = nodeId ?? ""; + DocumentRef = documentRef ?? ""; + DocumentSchema = documentSchema ?? ""; + MaterialAssetRef = materialAssetRef ?? ""; + RenderChannel = renderChannel ?? ""; + CompositeMode = compositeMode ?? ""; + Quality = quality ?? ""; + Props = props ?? new Dictionary(StringComparer.Ordinal); + } + + public string NodeId { get; } + public string DocumentRef { get; } + public string DocumentSchema { get; } + public string MaterialAssetRef { get; } + public string RenderChannel { get; } + public string CompositeMode { get; } + public string Quality { get; } + public IReadOnlyDictionary Props { get; } } public sealed class EveUnityPlayableWorldEntity diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs new file mode 100644 index 0000000..e40a392 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs @@ -0,0 +1,10 @@ +using System; +using GameCult.Eve.PluginFields; + +namespace GameCult.Eve.UnityScene.Fields +{ + public interface IEveUnityFieldsSplatsDocumentSource + { + event Action FieldsSplatsAvailable; + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta new file mode 100644 index 0000000..608135d --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsDocumentSource.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 01897652f3f84ae0be3a99c9f9837d9d +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs new file mode 100644 index 0000000..69ed109 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -0,0 +1,291 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using GameCult.Eve.PluginFields; +using UnityEngine; +using UnityEngine.Rendering; + +#nullable enable + +namespace GameCult.Eve.UnityScene.Fields +{ + public sealed class EveUnityFieldsVolumeRenderer : MonoBehaviour + { + private EveUnityPlayableWorldClientHost? _host; + private IEveUnityFieldsSplatsDocumentSource? _source; + private EveFieldsSplatsDocument? _document; + private EveFieldsSplatLayerRenderer? _layers; + private readonly List _targets = new List(); + private Material? _material; + private Shader? _sourceShader; + private RenderTexture? _cloudTexture; + private CommandBuffer? _commands; + private string _activeNodeId = ""; + + public long PresentedFrameId { get; private set; } = -1; + public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null); + + public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) + { + if (host == null) throw new ArgumentNullException(nameof(host)); + if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; + _host = host; + _source = source; + if (_source != null) _source.FieldsSplatsAvailable += OnFieldsSplatsAvailable; + enabled = _source != null; + } + + private void OnEnable() => RenderPipelineManager.endCameraRendering += OnEndCameraRendering; + + private void OnDisable() + { + RenderPipelineManager.endCameraRendering -= OnEndCameraRendering; + ReleaseRenderState(); + } + + private void OnDestroy() + { + if (_source != null) _source.FieldsSplatsAvailable -= OnFieldsSplatsAvailable; + _source = null; + _host = null; + } + + private void OnFieldsSplatsAvailable(EveFieldsSplatsDocument document) + { + if (document == null || document.FrameId < PresentedFrameId) return; + _document = document; + PresentedFrameId = document.FrameId; + RenderLayers(document); + } + + private void RenderLayers(EveFieldsSplatsDocument document) + { + var field = ActiveField(); + if (field == null) return; + EnsureLayerRenderer(); + EnsureTargets(field); + var width = PositiveInt(field, "textureWidth", 512); + var height = PositiveInt(field, "textureHeight", 512); + _layers!.Render(document, _targets, width, height); + if (!EnsureMaterial(field)) return; + ApplyFieldBindings(field, document); + } + + private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera) + { + if (_host == null || _document == null || camera == null || + _host.ActiveCameraTransform != camera.transform) + return; + var field = ActiveField(); + if (field == null || !EnsureMaterial(field)) return; + ApplyFieldBindings(field, _document); + camera.depthTextureMode |= DepthTextureMode.Depth; + EnsureCloudTexture(camera, field); + 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)); + } + + 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); + + _commands ??= new CommandBuffer { name = "Eve Fields Volume" }; + _commands.Clear(); + _commands.SetRenderTarget(_cloudTexture); + _commands.ClearRenderTarget(false, true, Color.clear); + _commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1); + _commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget); + _commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1); + context.ExecuteCommandBuffer(_commands); + } + + private EveUnityFieldVolumeProjection? ActiveField() => + _host?.ActiveWorld?.FieldVolumes.FirstOrDefault(field => + string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal)); + + private bool EnsureMaterial(EveUnityFieldVolumeProjection field) + { + var provider = _host?.NativeAssetProvider; + if (provider == null || string.IsNullOrWhiteSpace(field.MaterialAssetRef)) return false; + var shader = provider.ResolveAsset( + new EveUnityPlayableWorldAssetBinding(field.MaterialAssetRef, "", "provider-asset-ref"), + typeof(Shader)) as Shader; + if (shader == null || !shader.isSupported) 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) + { + 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); + } + 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); + } + foreach (var keyword in Prop(field, "shaderKeywords").Split(';')) + if (!string.IsNullOrWhiteSpace(keyword)) _material.EnableKeyword(keyword.Trim()); + return true; + } + + private void ApplyFieldBindings(EveUnityFieldVolumeProjection field, EveFieldsSplatsDocument document) + { + if (_material == null) return; + var bindings = ParseBindings(Prop(field, "layerBindings")); + 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)); + } + } + + private void EnsureTargets(EveUnityFieldVolumeProjection field) + { + var bindings = ParseBindings(Prop(field, "layerBindings")); + if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && + _targets.Count == bindings.Count) return; + _targets.Clear(); + foreach (var binding in bindings) + _targets.Add(new EveFieldsSplatLayerTarget { LayerKey = binding.Key, UseMipMaps = true }); + } + + private void EnsureLayerRenderer() + { + if (_layers != null) return; + _layers = GetComponent(); + if (_layers == null) _layers = gameObject.AddComponent(); + } + + private void EnsureCloudTexture(Camera camera, EveUnityFieldVolumeProjection field) + { + var downsample = Mathf.Clamp(NonNegativeInt(field, "downsample", 1), 0, 3); + var width = Mathf.Max(1, camera.pixelWidth >> downsample); + var height = Mathf.Max(1, camera.pixelHeight >> downsample); + if (_cloudTexture != null && _cloudTexture.width == width && _cloudTexture.height == height) return; + if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); } + _cloudTexture = new RenderTexture(width, height, 0, RenderTextureFormat.ARGBFloat) + { + name = "Eve Fields Volume", + filterMode = FilterMode.Bilinear, + wrapMode = TextureWrapMode.Clamp + }; + _cloudTexture.Create(); + } + + private void ReleaseRenderState() + { + if (_commands != null) { _commands.Release(); _commands = null; } + if (_cloudTexture != null) { _cloudTexture.Release(); Destroy(_cloudTexture); _cloudTexture = null; } + if (_material != null) { Destroy(_material); _material = null; } + _sourceShader = null; + _activeNodeId = ""; + PresentedFrameId = -1; + } + + private void SetMatrix(EveUnityFieldVolumeProjection field, string propKey, Matrix4x4 value) + { + var property = Prop(field, propKey); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetMatrix(property, value); + } + + private void SetVector(EveUnityFieldVolumeProjection field, string propKey, Vector4 value) + { + var property = Prop(field, propKey); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetVector(property, value); + } + + private void SetFloat(EveUnityFieldVolumeProjection field, string propKey, float value) + { + var property = Prop(field, propKey); + if (!string.IsNullOrWhiteSpace(property)) _material!.SetFloat(property, value); + } + + private static Vector4 ProjectionExtents(Camera camera) + { + var y = camera.orthographic ? camera.orthographicSize : Mathf.Tan(0.5f * Mathf.Deg2Rad * camera.fieldOfView); + return new Vector4(y * camera.aspect, y, 0f, 0f); + } + + private static float Halton(int index, int radix) + { + var result = 0f; + var fraction = 1f / radix; + while (index > 0) { result += (index % radix) * fraction; index /= radix; fraction /= radix; } + return result; + } + + private static Dictionary ParseBindings(string value) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var pair in (value ?? "").Split(';')) + { + var separator = pair.IndexOf('='); + if (separator <= 0 || separator >= pair.Length - 1) continue; + result[pair.Substring(0, separator).Trim()] = pair.Substring(separator + 1).Trim(); + } + return result; + } + + private static string Prop(EveUnityFieldVolumeProjection field, string key) => + field.Props.TryGetValue(key, out var value) ? value ?? "" : ""; + + private static int PositiveInt(EveUnityFieldVolumeProjection field, string key, int fallback) => + int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 ? value : fallback; + + private static int NonNegativeInt(EveUnityFieldVolumeProjection field, string key, int fallback) => + int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value >= 0 ? value : fallback; + + private static bool TryFloat(string value, out float result) => + float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && !float.IsNaN(result) && !float.IsInfinity(result); + + private static bool TryVector(string value, out Vector4 result) + { + result = default; + var values = (value ?? "").Split(','); + if (values.Length < 2 || values.Length > 4) return false; + var parsed = new float[4]; + for (var index = 0; index < values.Length; index++) + if (!TryFloat(values[index], out parsed[index])) return false; + result = new Vector4(parsed[0], parsed[1], parsed[2], parsed[3]); + return true; + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta new file mode 100644 index 0000000..bf9a46e --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9c5d41ca3a3a4335b1b82104bba3e20a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 959b571..0cbb1a8 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -23,6 +23,24 @@ namespace GameCult.Eve.UnityScene.Tests assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); } + [Test] + public void CanonicalFieldsPackageOwnsTheCultMeshWireDocument() + { + Assert.That(typeof(EveFieldsSplatsDocument).GetCustomAttributes(false) + .Any(attribute => attribute.GetType().Name == "CultDocumentAttribute"), Is.True); + Assert.That(new EveFieldsSplatsDocument().Schema, Is.EqualTo(EveFieldsSchemas.Splats)); + Assert.That(typeof(EveFieldsSplatsDocument).Assembly.GetReferencedAssemblies() + .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); + } + + [Test] + public void VolumeRendererConsumesCanonicalFieldsWithoutProviderCode() + { + Assert.That(typeof(EveUnityFieldsVolumeRenderer).Assembly.GetReferencedAssemblies() + .Any(assembly => assembly.Name?.Contains("Aetheria", StringComparison.OrdinalIgnoreCase) == true), Is.False); + Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); + } + [Test] public void PackageContainsGenericFieldsShader() { 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 0f7d926..6bc1684 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveUnitySceneSurfaceLowererTests.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; using GameCult.Caching; +using GameCult.Eve.PluginFields; using GameCult.Eve.Surface; using GameCult.Mesh; using NUnit.Framework; @@ -421,6 +422,10 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(projection.PlayableWorld.FocusCommand, Is.EqualTo("aetheria.daemon.focus")); Assert.That(projection.PlayableWorld.TargetCommand, Is.EqualTo("aetheria.daemon.target")); Assert.That(projection.PlayableWorld.EntityCount, Is.EqualTo(3)); + Assert.That(projection.PlayableWorld.FieldVolumes.Count, Is.EqualTo(1)); + Assert.That(projection.PlayableWorld.FieldVolumes[0].DocumentRef, Is.EqualTo("cultmesh://aetheria/world/fog-splats")); + Assert.That(projection.PlayableWorld.FieldVolumes[0].MaterialAssetRef, Is.EqualTo("shader.environment.gravity-fog")); + Assert.That(projection.PlayableWorld.FieldVolumes[0].RenderChannel, Is.EqualTo("world.transparent")); var player = FindEntity(projection.PlayableWorld, "player-vanguard"); Assert.That(player.EntityKind, Is.EqualTo("player")); @@ -1864,6 +1869,21 @@ namespace GameCult.Eve.UnityScene.Tests }, Array.Empty())); + playableChildren.Add(new EveSurfaceComponent( + "aetheria.daemon.game.gravity-fog", + "field.volume3d", + new Dictionary(StringComparer.Ordinal) + { + ["documentRef"] = "cultmesh://aetheria/world/fog-splats", + ["documentSchema"] = EveFieldsSchemas.Splats, + ["materialAssetRef"] = "shader.environment.gravity-fog", + ["renderChannel"] = "world.transparent", + ["compositeMode"] = "premultiplied-alpha", + ["quality"] = "normal", + ["layerBindings"] = "fog.surface_height=_NebulaSurfaceHeight;fog.tint=_NebulaTint" + }, + Array.Empty())); + return new EveSurfaceDocument( "aetheria", "game.runtime", diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 4bfe911..1e161eb 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,12 +1,12 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.32", + "version": "0.3.33", "unity": "2021.3", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": { "org.gamecult.cultlib": "1.0.13", - "org.gamecult.eve.plugin-fields": "0.1.1", + "org.gamecult.eve.plugin-fields": "0.2.0", "org.gamecult.eve.surface": "0.2.2", "com.unity.mathematics": "1.3.2", "com.unity.modules.particlesystem": "1.0.0"