Lower provider-owned volumetric fields
This commit is contained in:
parent
fbe326f211
commit
852458dade
12 changed files with 474 additions and 4 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
"references": [
|
||||
"GameCult.Eve.Surface",
|
||||
"GameCult.Eve.UnityScene",
|
||||
"GameCult.Eve.PluginFields",
|
||||
"UnityEngine.AssetBundleModule"
|
||||
],
|
||||
"includePlatforms": [],
|
||||
|
|
|
|||
|
|
@ -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++;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
using System;
|
||||
using GameCult.Eve.PluginFields;
|
||||
|
||||
namespace GameCult.Eve.UnityScene.Fields
|
||||
{
|
||||
public interface IEveUnityFieldsSplatsDocumentSource
|
||||
{
|
||||
event Action<EveFieldsSplatsDocument> FieldsSplatsAvailable;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 01897652f3f84ae0be3a99c9f9837d9d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9c5d41ca3a3a4335b1b82104bba3e20a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Reference in a new issue