Schedule Eve world effects through URP
This commit is contained in:
parent
2f05a81517
commit
b8259a8e52
8 changed files with 137 additions and 22 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Camera>();
|
||||
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<UniversalCameraData>();
|
||||
if (cameraData.camera == null || cameraData.camera != _owner._camera) return;
|
||||
var resourceData = frameData.Get<UniversalResourceData>();
|
||||
if (resourceData.isActiveTargetBackBuffer) return;
|
||||
var colorTarget = resourceData.activeColorTexture;
|
||||
if (!colorTarget.IsValid()) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<IEveUnityRenderPassSource> Sources =
|
||||
new List<IEveUnityRenderPassSource>();
|
||||
|
||||
[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);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 340385338d304b5aaf069562d4e7b457
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -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) =>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
Reference in a new issue