From b8259a8e52b3d024801f088a9e49072bddc6de0b Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Sat, 18 Jul 2026 16:36:28 +0100 Subject: [PATCH] Schedule Eve world effects through URP --- .../org.gamecult.eve.unity-scene/README.md | 8 +++ .../EveUnityAdaptiveExposureRenderer.cs | 18 ++++-- .../Runtime/EveUnityRendererFeature.cs | 59 +++++++++++++++++++ .../Runtime/EveUnityRendererFeature.cs.meta | 11 ++++ .../Fields/EveUnityFieldsParticleRenderer.cs | 18 +++--- .../Fields/EveUnityFieldsVolumeRenderer.cs | 16 ++--- .../EveFieldsUnitySceneProjectionTests.cs | 27 +++++++++ .../org.gamecult.eve.unity-scene/package.json | 2 +- 8 files changed, 137 insertions(+), 22 deletions(-) create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs create mode 100644 packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index 1c2dce7..e68c649 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -15,6 +15,14 @@ selection filters; they are not product knowledge required by the client. The package never imports provider product assemblies. +Add `EveUnityRendererFeature` to the Universal Renderer used by Eve cameras. +The feature is the single URP-owned scheduling point for provider-advertised +volume, particle, and exposure programs. The lowerers use Unity 6 RenderGraph +resource declarations; effects that sample camera color request an intermediate +color texture and never read the back buffer. Do not schedule these effects from +`RenderPipelineManager` callbacks. The provider owns programs, assets, and +parameters; the Universal Renderer owns when and where they execute. + Advertised input bindings are performed from the provider's semantic action catalog. Direct and chord gestures submit once per press edge; held values submit their advertised press/release scalar. `view-direction.v1` samples the diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs index 04e1f0b..281d5e8 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityAdaptiveExposureRenderer.cs @@ -8,7 +8,7 @@ using UnityEngine.Rendering.Universal; namespace GameCult.Eve.UnityScene { - public sealed class EveUnityAdaptiveExposureRenderer : MonoBehaviour + public sealed class EveUnityAdaptiveExposureRenderer : MonoBehaviour, IEveUnityRenderPassSource { private const int HistogramBins = 128; private ComputeShader? _compute; @@ -32,6 +32,7 @@ namespace GameCult.Eve.UnityScene public bool IsConfigured { get; private set; } public static RenderPassEvent ExposureRenderPassEvent => (RenderPassEvent)((int)RenderPassEvent.BeforeRenderingPostProcessing + 1); + int IEveUnityRenderPassSource.RenderOrder => 300; public void Configure( float lowPercent, @@ -58,22 +59,23 @@ namespace GameCult.Eve.UnityScene private void OnEnable() { _camera = GetComponent(); - RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; + EveUnityRenderPassRegistry.Register(this); } private void OnDisable() { - RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; + EveUnityRenderPassRegistry.Unregister(this); ReleaseResources(); } private void OnDestroy() => ReleaseResources(); - private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) + bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) { - if (!IsConfigured || _camera == null || camera != _camera) return; + if (!IsConfigured || _camera == null || camera != _camera) return false; _pass ??= new AdaptiveExposurePass(this); - camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_pass); + renderer.EnqueuePass(_pass); + return true; } private bool EnsureResources() @@ -195,6 +197,9 @@ namespace GameCult.Eve.UnityScene { _owner = owner; renderPassEvent = ExposureRenderPassEvent; + // Histogram construction samples the active camera color. URP must therefore + // provide an intermediate color texture instead of exposing the back buffer. + requiresIntermediateTexture = true; } public override void RecordRenderGraph(RenderGraph renderGraph, ContextContainer frameData) @@ -202,6 +207,7 @@ namespace GameCult.Eve.UnityScene var cameraData = frameData.Get(); if (cameraData.camera == null || cameraData.camera != _owner._camera) return; var resourceData = frameData.Get(); + if (resourceData.isActiveTargetBackBuffer) return; var colorTarget = resourceData.activeColorTexture; if (!colorTarget.IsValid()) return; diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs new file mode 100644 index 0000000..c94cf54 --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using UnityEngine; +using UnityEngine.Rendering.Universal; + +#nullable enable + +namespace GameCult.Eve.UnityScene +{ + internal interface IEveUnityRenderPassSource + { + int RenderOrder { get; } + bool TryEnqueuePass(Camera camera, ScriptableRenderer renderer); + } + + internal static class EveUnityRenderPassRegistry + { + private static readonly List Sources = + new List(); + + [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.SubsystemRegistration)] + private static void Reset() => Sources.Clear(); + + internal static void Register(IEveUnityRenderPassSource source) + { + if (source == null) throw new ArgumentNullException(nameof(source)); + if (Sources.Contains(source)) return; + Sources.Add(source); + Sources.Sort(static (left, right) => left.RenderOrder.CompareTo(right.RenderOrder)); + } + + internal static void Unregister(IEveUnityRenderPassSource source) => Sources.Remove(source); + + internal static void Enqueue(Camera camera, ScriptableRenderer renderer) + { + for (var index = 0; index < Sources.Count; index++) + Sources[index].TryEnqueuePass(camera, renderer); + } + } + + /// + /// URP-owned scheduling point for generic Eve world effects. + /// Add this feature to the Universal Renderer used by Eve cameras. + /// Provider surfaces supply effect programs, assets, and parameters; this feature only lowers them. + /// + public sealed class EveUnityRendererFeature : ScriptableRendererFeature + { + public override void Create() + { + } + + public override void AddRenderPasses(ScriptableRenderer renderer, ref RenderingData renderingData) + { + var camera = renderingData.cameraData.camera; + if (camera == null || renderingData.cameraData.cameraType != CameraType.Game) return; + EveUnityRenderPassRegistry.Enqueue(camera, renderer); + } + } +} diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta new file mode 100644 index 0000000..82fdbfc --- /dev/null +++ b/packages/org.gamecult.eve.unity-scene/Runtime/EveUnityRendererFeature.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 340385338d304b5aaf069562d4e7b457 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs index ca43dc5..803bfd1 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsParticleRenderer.cs @@ -12,7 +12,7 @@ using UnityEngine.Rendering.Universal; namespace GameCult.Eve.UnityScene.Fields { - public sealed class EveUnityFieldsParticleRenderer : MonoBehaviour + public sealed class EveUnityFieldsParticleRenderer : MonoBehaviour, IEveUnityRenderPassSource { private EveUnityPlayableWorldClientHost? _host; private IEveUnityFieldsSplatsDocumentSource? _source; @@ -42,6 +42,7 @@ namespace GameCult.Eve.UnityScene.Fields 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) { @@ -53,11 +54,11 @@ namespace GameCult.Eve.UnityScene.Fields enabled = _source != null; } - private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; + private void OnEnable() => EveUnityRenderPassRegistry.Register(this); private void OnDisable() { - RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; + EveUnityRenderPassRegistry.Unregister(this); ReleaseState(); } @@ -75,15 +76,16 @@ namespace GameCult.Eve.UnityScene.Fields PresentedFrameId = document.FrameId; } - private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) + bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) { if (_host == null || _document == null || camera == null || _host.ActiveCameraTransform != camera.transform) - return; + return false; var field = ActiveField(); - if (field == null || !PrepareFrame(field, camera, _document)) return; + if (field == null || !PrepareFrame(field, camera, _document)) return false; _renderPass ??= new EveUnityFieldsParticlePass(this); - camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_renderPass); + renderer.EnqueuePass(_renderPass); + return true; } private EveUnityFieldParticlesProjection? ActiveField() => @@ -488,7 +490,7 @@ namespace GameCult.Eve.UnityScene.Fields data.Owner = _owner; data.ColorTarget = resources.activeColorTexture; data.DepthTarget = resources.activeDepthTexture; - builder.UseTexture(data.ColorTarget, AccessFlags.ReadWrite); + 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) => diff --git a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs index 4cfd8d1..12f098d 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -12,7 +12,7 @@ using UnityEngine.Rendering.Universal; namespace GameCult.Eve.UnityScene.Fields { - public sealed class EveUnityFieldsVolumeRenderer : MonoBehaviour + public sealed class EveUnityFieldsVolumeRenderer : MonoBehaviour, IEveUnityRenderPassSource { private EveUnityPlayableWorldClientHost? _host; private IEveUnityFieldsSplatsDocumentSource? _source; @@ -40,6 +40,7 @@ namespace GameCult.Eve.UnityScene.Fields public bool UsesTemporalHistory => HasTemporalProgram(); public Vector2 LastGridCenter { get; private set; } public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing; + int IEveUnityRenderPassSource.RenderOrder => 100; public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source) { @@ -51,11 +52,11 @@ namespace GameCult.Eve.UnityScene.Fields enabled = _source != null; } - private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering; + private void OnEnable() => EveUnityRenderPassRegistry.Register(this); private void OnDisable() { - RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering; + EveUnityRenderPassRegistry.Unregister(this); ReleaseRenderState(); } @@ -114,13 +115,14 @@ namespace GameCult.Eve.UnityScene.Fields return true; } - private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera) + bool IEveUnityRenderPassSource.TryEnqueuePass(Camera camera, ScriptableRenderer renderer) { if (_host == null || _document == null || camera == null || _host.ActiveCameraTransform != camera.transform) - return; + return false; _renderPass ??= new EveUnityFieldsVolumePass(this); - camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_renderPass); + renderer.EnqueuePass(_renderPass); + return true; } private void RenderVolume( @@ -478,7 +480,7 @@ namespace GameCult.Eve.UnityScene.Fields passData.ColorTarget = colorTarget; passData.DepthTarget = resourceData.activeDepthTexture; - builder.UseTexture(passData.ColorTarget, AccessFlags.ReadWrite); + builder.UseTexture(passData.ColorTarget, AccessFlags.Write); if (passData.DepthTarget.IsValid()) builder.UseTexture(passData.DepthTarget, AccessFlags.Read); builder.AllowPassCulling(false); diff --git a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs index 242112d..9e93c2a 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -155,6 +155,15 @@ namespace GameCult.Eve.UnityScene.Tests Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null); } + [Test] + public void UrpRendererFeatureOwnsGenericWorldEffectScheduling() + { + Assert.That(typeof(EveUnityRendererFeature).IsSubclassOf(typeof(ScriptableRendererFeature)), Is.True); + AssertRenderOrder(typeof(EveUnityFieldsVolumeRenderer), 100); + AssertRenderOrder(typeof(EveUnityFieldsParticleRenderer), 200); + AssertRenderOrder(typeof(EveUnityAdaptiveExposureRenderer), 300); + } + [Test] public void VolumeRendererCompositesBeforeProviderPostProcessing() { @@ -163,6 +172,24 @@ namespace GameCult.Eve.UnityScene.Tests Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing)); } + private static void AssertRenderOrder(Type sourceType, int expected) + { + var schedulingInterface = sourceType.GetInterfaces().Single(type => + string.Equals(type.Name, "IEveUnityRenderPassSource", StringComparison.Ordinal)); + var property = schedulingInterface.GetProperty("RenderOrder"); + Assert.That(property, Is.Not.Null); + var gameObject = new GameObject("eve-render-pass-order-test"); + try + { + var component = gameObject.AddComponent(sourceType); + Assert.That(property!.GetValue(component), Is.EqualTo(expected)); + } + finally + { + UnityEngine.Object.DestroyImmediate(gameObject); + } + } + [Test] public void ParticleRendererExecutesProviderProgramWithoutProviderCode() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index f601fcc..f7589b9 100644 --- a/packages/org.gamecult.eve.unity-scene/package.json +++ b/packages/org.gamecult.eve.unity-scene/package.json @@ -1,7 +1,7 @@ { "name": "org.gamecult.eve.unity-scene", "displayName": "Eve Unity Scene Lowering", - "version": "0.3.65", + "version": "0.3.66", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {