using System; using System.Collections.Generic; using System.Globalization; using System.Linq; using GameCult.Eve.PluginFields; using UnityEngine; using UnityEngine.Rendering; using UnityEngine.Rendering.RenderGraphModule; using UnityEngine.Rendering.Universal; #nullable enable namespace GameCult.Eve.UnityScene.Fields { public sealed class EveUnityFieldsParticleRenderer : MonoBehaviour, IEveUnityRenderPassSource { private EveUnityPlayableWorldClientHost? _host; private IEveUnityFieldsSplatsDocumentSource? _source; private EveFieldsSplatsDocument? _document; private EveFieldsSplatLayerRenderer? _layers; private GameObject? _layerRoot; private readonly List _targets = new List(); private ComputeShader? _computeSource; private ComputeShader? _compute; private Material? _materialSource; private Material? _material; private IReadOnlyDictionary? _computeMetadata; private IReadOnlyDictionary? _materialMetadata; private readonly Dictionary _assetTextures = new Dictionary(StringComparer.Ordinal); private readonly Dictionary _materialAssetTextures = new Dictionary(StringComparer.Ordinal); private ComputeBuffer? _particles; private ComputeBuffer? _quadPoints; private EveUnityFieldsParticlePass? _renderPass; private string _activeNodeId = ""; private long _rasterizedFrameId = -1; private Vector2 _rasterizedCenter = new Vector2(float.NaN, float.NaN); public long PresentedFrameId { get; private set; } = -1; public int ParticleCount { get; private set; } public int DispatchCount { get; private set; } public int DrawCount { get; private set; } public Vector2 LastGridCenter { get; private set; } public bool ProgramReady => _compute != null && _material != null && _particles != null && _quadPoints != null; public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; int IEveUnityRenderPassSource.RenderOrder => 200; 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() => EveUnityRenderPassRegistry.Register(this); private void OnDisable() { EveUnityRenderPassRegistry.Unregister(this); ReleaseState(); } 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; } bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) { if (_host == null || _document == null || camera == null || _host.ActiveCameraTransform != camera.transform) return false; var field = ActiveField(); if (field == null || !PrepareFrame(field, camera, _document)) return false; _renderPass ??= new EveUnityFieldsParticlePass(this); renderer.EnqueuePass(_renderPass); return true; } private EveUnityFieldParticlesProjection? ActiveField() => _host?.ActiveWorld?.FieldParticles.FirstOrDefault(field => string.Equals(field.DocumentSchema, EveFieldsSchemas.Splats, StringComparison.Ordinal)); private bool PrepareFrame( EveUnityFieldParticlesProjection field, Camera camera, EveFieldsSplatsDocument document) { if (!EnsurePrograms(field) || !EnsureBuffers(field)) return false; EnsureTargets(field); EnsureLayerRenderer(); if (_layers == null) return false; var baseWidth = PositiveInt(field, "textureWidth", 512); var baseHeight = PositiveInt(field, "textureHeight", 512); var snapLayer = Prop(field, "viewportSnapLayer"); var snapTarget = _targets.FirstOrDefault(target => string.Equals(target.LayerKey, snapLayer, StringComparison.Ordinal)); var snapWidth = snapTarget == null ? 0 : Mathf.Max(1, Mathf.RoundToInt(baseWidth * Mathf.Max(0.01f, snapTarget.WidthScale))); var snapHeight = snapTarget == null ? 0 : Mathf.Max(1, Mathf.RoundToInt(baseHeight * Mathf.Max(0.01f, snapTarget.HeightScale))); if (!EveUnityFieldsViewportFrame.TryResolve( document, field.Props, new Vector2(camera.transform.position.x, camera.transform.position.z), snapWidth, snapHeight, out var projected, out var center)) return false; var viewportWidth = (float)(projected.Viewport.MaxX - projected.Viewport.MinX); var viewportHeight = (float)(projected.Viewport.MaxY - projected.Viewport.MinY); if (_rasterizedFrameId != document.FrameId || _rasterizedCenter != center) { _layers.Render(projected, _targets, baseWidth, baseHeight); _rasterizedFrameId = document.FrameId; _rasterizedCenter = center; } ApplyProgramBindings(field, document, center, viewportWidth, viewportHeight); var kernel = ProgramKernel("update"); if (kernel < 0) return false; var threadGroupSize = PositiveInt(field, "threadGroupSize", 1); var groups = Mathf.CeilToInt((float)ParticleCount / threadGroupSize); if (groups <= 0) return false; _compute!.Dispatch(kernel, groups, 1, 1); DispatchCount++; LastGridCenter = center; return true; } private bool EnsurePrograms(EveUnityFieldParticlesProjection field) { var provider = _host?.NativeAssetProvider; if (provider == null || provider is not IEveUnityNativeAssetMetadataProvider metadataProvider || string.IsNullOrWhiteSpace(field.ComputeProgramAssetRef) || string.IsNullOrWhiteSpace(field.MaterialAssetRef)) return false; var computeBinding = new EveUnityPlayableWorldAssetBinding( field.ComputeProgramAssetRef, "", "provider-asset-ref"); var materialBinding = new EveUnityPlayableWorldAssetBinding( field.MaterialAssetRef, "", "provider-asset-ref"); var compute = provider.ResolveAsset(computeBinding, typeof(ComputeShader)) as ComputeShader; var material = provider.ResolveAsset(materialBinding, typeof(Material)) as Material; if (compute == null || material == null || material.shader == null || !material.shader.isSupported || !metadataProvider.TryResolveAssetMetadata(computeBinding, out var computeMetadata) || !metadataProvider.TryResolveAssetMetadata(materialBinding, out var materialMetadata) || !TryValidateProgramMetadata(computeMetadata, materialMetadata, out _) || !TryValidateAdvertisedContract(field, compute, material, computeMetadata, materialMetadata)) return false; var resolvedTextures = new Dictionary(StringComparer.Ordinal); 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) return false; resolvedTextures[binding.Value] = texture; } var resolvedMaterialTextures = new Dictionary(StringComparer.Ordinal); foreach (var binding in ParseBindings(Prop(field, "materialAssetTextureBindings"))) { var texture = provider.ResolveAsset( new EveUnityPlayableWorldAssetBinding(binding.Key, "", "provider-asset-ref"), typeof(Texture)) as Texture; if (texture == null) return false; resolvedMaterialTextures[binding.Value] = texture; } if (ReferenceEquals(compute, _computeSource) && ReferenceEquals(material, _materialSource) && string.Equals(field.NodeId, _activeNodeId, StringComparison.Ordinal) && ProgramReady && resolvedTextures.Count == _assetTextures.Count && resolvedTextures.All(binding => _assetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value)) && resolvedMaterialTextures.Count == _materialAssetTextures.Count && resolvedMaterialTextures.All(binding => _materialAssetTextures.TryGetValue(binding.Key, out var current) && ReferenceEquals(current, binding.Value))) return true; ReleaseProgramsAndBuffers(); _computeSource = compute; _materialSource = material; _computeMetadata = computeMetadata; _materialMetadata = materialMetadata; _activeNodeId = field.NodeId; _compute = Instantiate(compute); _material = new Material(material) { hideFlags = HideFlags.DontSave }; _assetTextures.Clear(); foreach (var binding in resolvedTextures) _assetTextures[binding.Key] = binding.Value; _materialAssetTextures.Clear(); foreach (var binding in resolvedMaterialTextures) { _materialAssetTextures[binding.Key] = binding.Value; SetMaterialTexture(binding.Key, binding.Value); } foreach (var feature in Prop(field, "features").Split(';')) { var logical = feature.Trim(); if (!string.IsNullOrWhiteSpace(logical) && TryComputeProgramValue($"unity.particles.feature.{logical}.keyword", out var keyword) && !string.IsNullOrWhiteSpace(keyword)) _compute.EnableKeyword(keyword); } return true; } public static bool TryValidateProgramMetadata( IReadOnlyDictionary? compute, IReadOnlyDictionary? material, out string error) { if (compute == null || material == null) return Fail("Field-particle programs require compute and material metadata.", out error); var computeKeys = new[] { "unity.particles.kernel.update", "unity.particles.bufferPort.particles", "unity.particles.texturePort.surfaceHeight", "unity.particles.texturePort.tint", "unity.particles.texturePort.hue", "unity.particles.vectorPort.viewportTransform", "unity.particles.vectorPort.timeVector", "unity.particles.floatPort.time", "unity.particles.floatPort.period", "unity.particles.floatPort.spacing", "unity.particles.intPort.span" }; if (computeKeys.Any(key => !compute.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value))) return Fail("Field-particle compute metadata is missing a required logical port.", out error); var materialKeys = new[] { "unity.particles.pass.render", "unity.particles.bufferPort.particles", "unity.particles.bufferPort.quadPoints" }; if (materialKeys.Any(key => !material.TryGetValue(key, out var value) || string.IsNullOrWhiteSpace(value)) || !int.TryParse(material["unity.particles.pass.render"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var pass) || pass < 0) return Fail("Field-particle material metadata is missing a valid render pass or buffer port.", out error); error = ""; return true; } private static bool TryValidateAdvertisedContract( EveUnityFieldParticlesProjection field, ComputeShader compute, Material material, IReadOnlyDictionary computeMetadata, IReadOnlyDictionary materialMetadata) { var kernelName = computeMetadata["unity.particles.kernel.update"]; if (!compute.HasKernel(kernelName)) return false; var kernel = compute.FindKernel(kernelName); compute.GetKernelThreadGroupSizes(kernel, out var nativeX, out var nativeY, out var nativeZ); if (!int.TryParse(Prop(field, "threadGroupSize"), NumberStyles.Integer, CultureInfo.InvariantCulture, out var advertisedGroupSize) || advertisedGroupSize <= 0 || nativeX != advertisedGroupSize || nativeY != 1 || nativeZ != 1) return false; if (!int.TryParse(materialMetadata["unity.particles.pass.render"], NumberStyles.Integer, CultureInfo.InvariantCulture, out var pass) || pass < 0 || pass >= material.passCount) return false; foreach (var binding in ParseBindingPairs(Prop(field, "layerBindings"))) if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; foreach (var binding in ParseBindingPairs(Prop(field, "assetTextureBindings"))) if (!HasPort(computeMetadata, "texturePort", binding.Value)) return false; if (!TryValidateMaterialPresentationContract(field, materialMetadata, out _)) return false; foreach (var parameter in ParseBindingPairs(Prop(field, "floatParameters"))) if (!HasPort(computeMetadata, "floatPort", parameter.Key) || !TryFloat(parameter.Value, out _)) return false; var floatParameters = ParseBindings(Prop(field, "floatParameters")); if (!floatParameters.TryGetValue("spacing", out var spacingValue) || !TryFloat(spacingValue, out var spacing) || !TryFloat(Prop(field, "cellWorldSize"), out var cellWorldSize) || Mathf.Abs(spacing - cellWorldSize) > 0.0001f || PositiveInt(field, "gravityTexelsPerCell", 0) != PositiveInt(field, "viewportSnapTexels", 0)) return false; foreach (var binding in ParseBindingPairs(Prop(field, "documentFloatBindings"))) { var pieces = binding.Value.Split(','); if (pieces.Length != 3 || !HasPort(computeMetadata, "floatPort", pieces[0].Trim()) || !TryFloat(pieces[1], out _) || !TryFloat(pieces[2], out _)) return false; } var timeVectorPort = Prop(field, "documentTimeVectorPort"); if (!string.IsNullOrWhiteSpace(timeVectorPort) && !HasPort(computeMetadata, "vectorPort", timeVectorPort)) return false; foreach (var feature in Prop(field, "features").Split(';')) { var logical = feature.Trim(); if (!string.IsNullOrWhiteSpace(logical) && (!computeMetadata.TryGetValue($"unity.particles.feature.{logical}.keyword", out var keyword) || string.IsNullOrWhiteSpace(keyword))) return false; } return PositiveInt(field, "span", 0) > 0 && PositiveInt(field, "particleStrideBytes", 0) > 0 && PositiveInt(field, "textureWidth", 0) > 0 && PositiveInt(field, "textureHeight", 0) > 0; } public static bool TryValidateMaterialPresentationContract( EveUnityFieldParticlesProjection field, IReadOnlyDictionary? materialMetadata, out string error) { if (field == null || materialMetadata == null) return Fail("Field-particle material presentation contract is missing.", out error); var textures = ParseBindings(Prop(field, "materialAssetTextureBindings")); foreach (var binding in textures) if (!HasPort(materialMetadata, "texturePort", binding.Value)) return Fail("Field-particle material texture binding has no native logical port.", out error); foreach (var binding in ParseBindingPairs(Prop(field, "materialViewportTextureScaleBindings"))) { if (!HasPort(materialMetadata, "vectorPort", binding.Key) || !textures.Values.Contains(binding.Value)) return Fail("Field-particle viewport texture-scale binding is incomplete.", out error); } var framePort = Prop(field, "materialRenderFrameIndexPort"); if (!string.IsNullOrWhiteSpace(framePort) && !HasPort(materialMetadata, "intPort", framePort)) return Fail("Field-particle render-frame binding has no native logical port.", out error); error = ""; return true; } private static bool HasPort( IReadOnlyDictionary metadata, string kind, string logical) => metadata.TryGetValue($"unity.particles.{kind}.{logical}", out var native) && !string.IsNullOrWhiteSpace(native); private bool EnsureBuffers(EveUnityFieldParticlesProjection field) { var span = PositiveInt(field, "span", 0); var stride = PositiveInt(field, "particleStrideBytes", 0); if (span <= 0 || span > 4096 || stride <= 0 || stride > 1024) return false; var count64 = (long)span * span; if (count64 <= 0 || count64 > 16_777_216) return false; var count = (int)count64; if (_particles != null && ParticleCount == count) return true; _particles?.Release(); _quadPoints?.Release(); _particles = new ComputeBuffer(count, stride, ComputeBufferType.Structured); _quadPoints = new ComputeBuffer(6, sizeof(float) * 3, ComputeBufferType.Structured); _quadPoints.SetData(new[] { new Vector3(-0.5f, 0.5f), new Vector3(0.5f, 0.5f), new Vector3(0.5f, -0.5f), new Vector3(0.5f, -0.5f), new Vector3(-0.5f, -0.5f), new Vector3(-0.5f, 0.5f) }); ParticleCount = count; return true; } private void ApplyProgramBindings( EveUnityFieldParticlesProjection field, EveFieldsSplatsDocument document, Vector2 center, float viewportWidth, float viewportHeight) { var kernel = ProgramKernel("update"); if (kernel < 0) return; SetComputeBuffer(kernel, "particles", _particles!); SetMaterialBuffer("particles", _particles!); SetMaterialBuffer("quadPoints", _quadPoints!); foreach (var binding in ParseBindings(Prop(field, "layerBindings"))) { var target = _targets.FirstOrDefault(value => string.Equals(value.LayerKey, binding.Key, StringComparison.Ordinal)); if (target?.TargetTexture != null) SetComputeTexture(kernel, binding.Value, target.TargetTexture); } foreach (var binding in _assetTextures) SetComputeTexture(kernel, binding.Key, binding.Value); SetComputeVector("viewportTransform", new Vector4(center.x, center.y, viewportWidth, viewportHeight)); var time = (float)document.SimulationTimeSeconds; var timeVectorPort = Prop(field, "documentTimeVectorPort"); if (!string.IsNullOrWhiteSpace(timeVectorPort)) SetComputeVector(timeVectorPort, new Vector4(time / 20f, time, time * 2f, time * 3f)); if (TryEvaluateDocumentFloatBindings( document, Prop(field, "documentFloatBindings"), out var documentFloats)) foreach (var binding in documentFloats) SetComputeFloat(binding.Key, binding.Value); foreach (var parameter in ParseBindings(Prop(field, "floatParameters"))) if (TryFloat(parameter.Value, out var value)) SetComputeFloat(parameter.Key, value); SetComputeInt("span", PositiveInt(field, "span", 0)); } private void RenderParticles( UnsafeCommandBuffer commands, Camera camera, TextureHandle colorTarget, TextureHandle depthTarget) { if (_host == null || _host.ActiveCameraTransform != camera.transform || !ProgramReady) return; var field = ActiveField(); if (field == null) return; foreach (var binding in ParseBindings(Prop(field, "materialViewportTextureScaleBindings"))) { if (!_materialAssetTextures.TryGetValue(binding.Value, out var texture) || texture.width <= 0 || texture.height <= 0) return; SetMaterialVector(binding.Key, new Vector4( (float)Math.Max(1, camera.pixelWidth) / texture.width, (float)Math.Max(1, camera.pixelHeight) / texture.height, 0f, 0f)); } var framePort = Prop(field, "materialRenderFrameIndexPort"); if (!string.IsNullOrWhiteSpace(framePort)) SetMaterialInt(framePort, Time.frameCount); if (depthTarget.IsValid()) commands.SetRenderTarget(colorTarget, depthTarget); else commands.SetRenderTarget(colorTarget); commands.SetViewport(camera.pixelRect); commands.DrawProcedural(Matrix4x4.identity, _material!, MaterialPass("render"), MeshTopology.Triangles, 6, ParticleCount); DrawCount++; } private void EnsureTargets(EveUnityFieldParticlesProjection field) { var bindings = ParseBindings(Prop(field, "layerBindings")); var descriptors = ParseBindings(Prop(field, "layerTargetDescriptors")); if (string.Equals(_activeNodeId, field.NodeId, StringComparison.Ordinal) && _targets.Count == bindings.Count && _targets.All(target => bindings.ContainsKey(target.LayerKey))) return; _targets.Clear(); foreach (var binding in bindings) { var target = new EveFieldsSplatLayerTarget { LayerKey = binding.Key }; if (descriptors.TryGetValue(binding.Key, out var descriptor)) EveUnityFieldsVolumeRenderer.TryApplyLayerTargetDescriptor(target, descriptor); _targets.Add(target); } } private void EnsureLayerRenderer() { if (_layers != null) return; _layerRoot = new GameObject("Eve Fields Particle Layers") { hideFlags = HideFlags.DontSave }; _layerRoot.transform.SetParent(transform, false); _layers = _layerRoot.AddComponent(); } private sealed class EveUnityFieldsParticlePass : ScriptableRenderPass { private readonly EveUnityFieldsParticleRenderer _owner; private sealed class PassData { internal Camera Camera = null!; internal EveUnityFieldsParticleRenderer Owner = null!; internal TextureHandle ColorTarget; internal TextureHandle DepthTarget; } internal EveUnityFieldsParticlePass(EveUnityFieldsParticleRenderer owner) { _owner = owner; renderPassEvent = CompositionRenderPassEvent; ConfigureInput(ScriptableRenderPassInput.Depth); } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) { var cameraData = frameData.Get(); if (cameraData.camera == null || _owner._host == null || _owner._host.ActiveCameraTransform != cameraData.camera.transform) return; var resources = frameData.Get(); if (!resources.activeColorTexture.IsValid()) return; using var builder = renderGraph.AddUnsafePass("Eve Fields Particles", out var data); data.Camera = cameraData.camera; data.Owner = _owner; data.ColorTarget = resources.activeColorTexture; data.DepthTarget = resources.activeDepthTexture; builder.UseTexture(data.ColorTarget, AccessFlags.Write); if (data.DepthTarget.IsValid()) builder.UseTexture(data.DepthTarget, AccessFlags.Read); builder.AllowPassCulling(false); builder.SetRenderFunc(static (PassData pass, UnsafeGraphContext context) => pass.Owner.RenderParticles(context.cmd, pass.Camera, pass.ColorTarget, pass.DepthTarget)); } } private int ProgramKernel(string logical) { if (_compute == null || !TryComputeProgramValue($"unity.particles.kernel.{logical}", out var name) || string.IsNullOrWhiteSpace(name) || !_compute.HasKernel(name)) return -1; return _compute.FindKernel(name); } private int MaterialPass(string logical) => TryMaterialProgramValue($"unity.particles.pass.{logical}", out var value) && int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var pass) && pass >= 0 ? pass : -1; private void SetComputeTexture(int kernel, string logical, Texture texture) { if (TryComputeProgramValue($"unity.particles.texturePort.{logical}", out var property)) _compute!.SetTexture(kernel, property, texture); } private void SetComputeBuffer(int kernel, string logical, ComputeBuffer buffer) { if (TryComputeProgramValue($"unity.particles.bufferPort.{logical}", out var property)) _compute!.SetBuffer(kernel, property, buffer); } private void SetMaterialBuffer(string logical, ComputeBuffer buffer) { if (TryMaterialProgramValue($"unity.particles.bufferPort.{logical}", out var property)) _material!.SetBuffer(property, buffer); } private void SetMaterialTexture(string logical, Texture texture) { if (TryMaterialProgramValue($"unity.particles.texturePort.{logical}", out var property)) _material!.SetTexture(property, texture); } private void SetMaterialVector(string logical, Vector4 value) { if (TryMaterialProgramValue($"unity.particles.vectorPort.{logical}", out var property)) _material!.SetVector(property, value); } private void SetMaterialInt(string logical, int value) { if (TryMaterialProgramValue($"unity.particles.intPort.{logical}", out var property)) _material!.SetInt(property, value); } private void SetComputeFloat(string logical, float value) { if (TryComputeProgramValue($"unity.particles.floatPort.{logical}", out var property)) _compute!.SetFloat(property, value); } private void SetComputeInt(string logical, int value) { if (TryComputeProgramValue($"unity.particles.intPort.{logical}", out var property)) _compute!.SetInt(property, value); } private void SetComputeVector(string logical, Vector4 value) { if (TryComputeProgramValue($"unity.particles.vectorPort.{logical}", out var property)) _compute!.SetVector(property, value); } private bool TryComputeProgramValue(string key, out string value) { value = ""; return _computeMetadata != null && _computeMetadata.TryGetValue(key, out value!); } private bool TryMaterialProgramValue(string key, out string value) { value = ""; return _materialMetadata != null && _materialMetadata.TryGetValue(key, out value!); } private void ReleaseState() { ReleaseProgramsAndBuffers(); if (_layerRoot != null) Destroy(_layerRoot); _layerRoot = null; _layers = null; _targets.Clear(); _document = null; PresentedFrameId = -1; DispatchCount = 0; DrawCount = 0; LastGridCenter = default; _rasterizedFrameId = -1; _rasterizedCenter = new Vector2(float.NaN, float.NaN); } private void ReleaseProgramsAndBuffers() { _particles?.Release(); _quadPoints?.Release(); _particles = null; _quadPoints = null; ParticleCount = 0; if (_compute != null) Destroy(_compute); if (_material != null) Destroy(_material); _compute = null; _material = null; _computeSource = null; _materialSource = null; _computeMetadata = null; _materialMetadata = null; _assetTextures.Clear(); _materialAssetTextures.Clear(); _activeNodeId = ""; } private static bool Fail(string message, out string error) { error = message; return false; } private static Dictionary ParseBindings(string value) { var result = new Dictionary(StringComparer.Ordinal); foreach (var pair in ParseBindingPairs(value)) result[pair.Key] = pair.Value; return result; } private static IReadOnlyList> ParseBindingPairs(string value) { var result = new List>(); foreach (var pair in (value ?? "").Split(';')) { var separator = pair.IndexOf('='); if (separator <= 0 || separator >= pair.Length - 1) continue; result.Add(new KeyValuePair( pair.Substring(0, separator).Trim(), pair.Substring(separator + 1).Trim())); } return result; } public static bool TryEvaluateDocumentFloatBindings( EveFieldsSplatsDocument document, string bindings, out IReadOnlyList> values) { var result = new List>(); foreach (var binding in ParseBindingPairs(bindings)) { if (!EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding( document, binding.Key, binding.Value, out var port, out var value)) { values = Array.Empty>(); return false; } result.Add(new KeyValuePair(port, value)); } values = result; return true; } private static string Prop(EveUnityFieldParticlesProjection field, string key) => field.Props.TryGetValue(key, out var value) ? value ?? "" : ""; private static int PositiveInt(EveUnityFieldParticlesProjection field, string key, int fallback) => int.TryParse(Prop(field, key), NumberStyles.Integer, CultureInfo.InvariantCulture, out var value) && value > 0 ? value : fallback; private static float PositiveFloat(EveUnityFieldParticlesProjection field, string key, float fallback) => TryFloat(Prop(field, key), out var value) && value > 0f ? value : fallback; private static bool TryFloat(string value, out float result) => float.TryParse(value, NumberStyles.Float, CultureInfo.InvariantCulture, out result) && float.IsFinite(result); } }