Lower provider-owned volumetric fields

This commit is contained in:
Meta Vens 2026-07-15 17:12:11 +01:00
commit 852458dade
12 changed files with 474 additions and 4 deletions

View file

@ -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<string, long> _lastFieldsFrameByRecord = new Dictionary<string, long>(StringComparer.Ordinal);
public EveUnityCultMeshLiveProviderTransport(
string replicaPath,
@ -110,6 +115,8 @@ namespace GameCult.Eve.UnityScene
public event Action<EveEntitySoaViewDocument, ICultMeshBodyReadLease>? EntityViewAvailable;
public event Action<EveFieldsSplatsDocument>? 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<EveFieldsSplatsDocument>(
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)

View file

@ -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<EntityViewLease> _pendingEntityViews = new ConcurrentQueue<EntityViewLease>();
private readonly ConcurrentQueue<EveFieldsSplatsDocument> _pendingFields = new ConcurrentQueue<EveFieldsSplatsDocument>();
public event Action<EveEntitySoaViewDocument, ICultMeshBodyReadLease>? EntityViewAvailable;
public event Action<EveFieldsSplatsDocument>? 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);
}

View file

@ -4,6 +4,7 @@
"references": [
"GameCult.Eve.Surface",
"GameCult.Eve.UnityScene",
"GameCult.Eve.PluginFields",
"UnityEngine.AssetBundleModule"
],
"includePlatforms": [],

View file

@ -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<EveUnityThermalHudSink>();
if (thermal == null) thermal = gameObject.AddComponent<EveUnityThermalPresenter>();
thermal.Bind(this, Runtime.GameObjectAssetProvider as IEveUnityNativeAssetProvider);
var fieldVolume = GetComponent<EveUnityFieldsVolumeRenderer>();
if (fieldVolume == null) fieldVolume = gameObject.AddComponent<EveUnityFieldsVolumeRenderer>();
fieldVolume.Bind(this, providerSurfaceDocuments as IEveUnityFieldsSplatsDocumentSource);
var presentation = Runtime.Connect();
ConnectionEpoch++;

View file

@ -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<EveUnityPlayableWorldEntity>();
var fieldVolumes = new List<EveUnityFieldVolumeProjection>();
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<EveUnityFieldVolumeProjection>? 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<EveUnityFieldVolumeProjection>();
}
public string WorldRootId { get; }
@ -506,6 +524,40 @@ namespace GameCult.Eve.UnityScene
public int EntityCount => Entities.Count;
public IReadOnlyList<EveUnityPlayableWorldEntity> Entities { get; }
public IReadOnlyList<EveUnityFieldVolumeProjection> FieldVolumes { get; }
}
public sealed class EveUnityFieldVolumeProjection
{
public EveUnityFieldVolumeProjection(
string nodeId,
string documentRef,
string documentSchema,
string materialAssetRef,
string renderChannel,
string compositeMode,
string quality,
IReadOnlyDictionary<string, string>? props = null)
{
NodeId = nodeId ?? "";
DocumentRef = documentRef ?? "";
DocumentSchema = documentSchema ?? "";
MaterialAssetRef = materialAssetRef ?? "";
RenderChannel = renderChannel ?? "";
CompositeMode = compositeMode ?? "";
Quality = quality ?? "";
Props = props ?? new Dictionary<string, string>(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<string, string> Props { get; }
}
public sealed class EveUnityPlayableWorldEntity

View file

@ -0,0 +1,10 @@
using System;
using GameCult.Eve.PluginFields;
namespace GameCult.Eve.UnityScene.Fields
{
public interface IEveUnityFieldsSplatsDocumentSource
{
event Action<EveFieldsSplatsDocument> FieldsSplatsAvailable;
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 01897652f3f84ae0be3a99c9f9837d9d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -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<EveFieldsSplatLayerTarget> _targets = new List<EveFieldsSplatLayerTarget>();
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<EveFieldsSplatLayerRenderer>();
if (_layers == null) _layers = gameObject.AddComponent<EveFieldsSplatLayerRenderer>();
}
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<string, string> ParseBindings(string value)
{
var result = new Dictionary<string, string>(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;
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 9c5d41ca3a3a4335b1b82104bba3e20a
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View file

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

View file

@ -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<EveSurfaceComponent>()));
playableChildren.Add(new EveSurfaceComponent(
"aetheria.daemon.game.gravity-fog",
"field.volume3d",
new Dictionary<string, string>(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<EveSurfaceComponent>()));
return new EveSurfaceDocument(
"aetheria",
"game.runtime",

View file

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