678 lines
33 KiB
C#
678 lines
33 KiB
C#
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<EveFieldsSplatLayerTarget> _targets = new List<EveFieldsSplatLayerTarget>();
|
|
private ComputeShader? _computeSource;
|
|
private ComputeShader? _compute;
|
|
private Material? _materialSource;
|
|
private Material? _material;
|
|
private IReadOnlyDictionary<string, string>? _computeMetadata;
|
|
private IReadOnlyDictionary<string, string>? _materialMetadata;
|
|
private readonly Dictionary<string, Texture> _assetTextures = new Dictionary<string, Texture>(StringComparer.Ordinal);
|
|
private readonly Dictionary<string, Texture> _materialAssetTextures = new Dictionary<string, Texture>(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<string, Texture>(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<string, Texture>(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<string, string>? compute,
|
|
IReadOnlyDictionary<string, string>? 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<string, string> computeMetadata,
|
|
IReadOnlyDictionary<string, string> 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<string, string>? 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<string, string> 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<EveFieldsSplatLayerRenderer>();
|
|
}
|
|
|
|
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<UniversalCameraData>();
|
|
if (cameraData.camera == null || _owner._host == null ||
|
|
_owner._host.ActiveCameraTransform != cameraData.camera.transform)
|
|
return;
|
|
var resources = frameData.Get<UniversalResourceData>();
|
|
if (!resources.activeColorTexture.IsValid()) return;
|
|
using var builder = renderGraph.AddUnsafePass<PassData>("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<string, string> ParseBindings(string value)
|
|
{
|
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
|
foreach (var pair in ParseBindingPairs(value)) result[pair.Key] = pair.Value;
|
|
return result;
|
|
}
|
|
|
|
private static IReadOnlyList<KeyValuePair<string, string>> ParseBindingPairs(string value)
|
|
{
|
|
var result = new List<KeyValuePair<string, string>>();
|
|
foreach (var pair in (value ?? "").Split(';'))
|
|
{
|
|
var separator = pair.IndexOf('=');
|
|
if (separator <= 0 || separator >= pair.Length - 1) continue;
|
|
result.Add(new KeyValuePair<string, string>(
|
|
pair.Substring(0, separator).Trim(),
|
|
pair.Substring(separator + 1).Trim()));
|
|
}
|
|
return result;
|
|
}
|
|
|
|
public static bool TryEvaluateDocumentFloatBindings(
|
|
EveFieldsSplatsDocument document,
|
|
string bindings,
|
|
out IReadOnlyList<KeyValuePair<string, float>> values)
|
|
{
|
|
var result = new List<KeyValuePair<string, float>>();
|
|
foreach (var binding in ParseBindingPairs(bindings))
|
|
{
|
|
if (!EveUnityFieldsVolumeRenderer.TryEvaluateDocumentFloatBinding(
|
|
document, binding.Key, binding.Value, out var port, out var value))
|
|
{
|
|
values = Array.Empty<KeyValuePair<string, float>>();
|
|
return false;
|
|
}
|
|
result.Add(new KeyValuePair<string, float>(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);
|
|
}
|
|
}
|