Composite provider volumes before post processing

This commit is contained in:
Meta Vens 2026-07-16 00:37:33 +01:00
commit d3840c9996
4 changed files with 92 additions and 21 deletions

View file

@ -69,4 +69,6 @@ keywords, and pass indices belong to the selected `unity-scene` asset variant
under `unity.volume.*` metadata. The lowerer fails closed when that native
program descriptor is absent or incomplete. This keeps provider shader ABIs
out of Eve's semantic surface while allowing a provider-owned shader to consume
portable Eve Fields documents.
portable Eve Fields documents. Volume composition runs as a generic URP render
pass before post-processing so the provider-advertised profile grades both the
native scene and the provider volume through the same camera pipeline.

View file

@ -5,6 +5,8 @@ using System.Linq;
using GameCult.Eve.PluginFields;
using UnityEngine;
using UnityEngine.Rendering;
using UnityEngine.Rendering.RenderGraphModule;
using UnityEngine.Rendering.Universal;
#nullable enable
@ -26,13 +28,14 @@ namespace GameCult.Eve.UnityScene.Fields
private readonly RenderTexture?[] _historyTextures = new RenderTexture?[2];
private int _historyTextureIndex = -1;
private Matrix4x4 _previousViewProjection;
private CommandBuffer? _commands;
private EveUnityFieldsVolumePass? _renderPass;
private string _activeNodeId = "";
public long PresentedFrameId { get; private set; } = -1;
public int PresentedLayerCount => _targets.Count(target => target.TargetTexture != null);
public long CompositeCount { get; private set; }
public bool UsesTemporalHistory => HasTemporalProgram();
public static RenderPassEvent CompositionRenderPassEvent => RenderPassEvent.BeforeRenderingPostProcessing;
public void Bind(EveUnityPlayableWorldClientHost host, IEveUnityFieldsSplatsDocumentSource? source)
{
@ -44,11 +47,11 @@ namespace GameCult.Eve.UnityScene.Fields
enabled = _source != null;
}
private void OnEnable() => RenderPipelineManager.endCameraRendering += OnEndCameraRendering;
private void OnEnable() => RenderPipelineManager.beginCameraRendering += OnBeginCameraRendering;
private void OnDisable()
{
RenderPipelineManager.endCameraRendering -= OnEndCameraRendering;
RenderPipelineManager.beginCameraRendering -= OnBeginCameraRendering;
ReleaseRenderState();
}
@ -80,7 +83,20 @@ namespace GameCult.Eve.UnityScene.Fields
ApplyFieldBindings(field, document);
}
private void OnEndCameraRendering(ScriptableRenderContext context, Camera camera)
private void OnBeginCameraRendering(ScriptableRenderContext context, Camera camera)
{
if (_host == null || _document == null || camera == null ||
_host.ActiveCameraTransform != camera.transform)
return;
_renderPass ??= new EveUnityFieldsVolumePass(this);
camera.GetUniversalAdditionalCameraData().scriptableRenderer.EnqueuePass(_renderPass);
}
private void RenderVolume(
UnsafeCommandBuffer commands,
Camera camera,
TextureHandle colorTarget,
TextureHandle depthTarget)
{
if (_host == null || _document == null || camera == null ||
_host.ActiveCameraTransform != camera.transform)
@ -120,23 +136,23 @@ namespace GameCult.Eve.UnityScene.Fields
}
SetTexturePort("cloud", finalCloudTexture);
_commands ??= new CommandBuffer { name = "Eve Fields Volume" };
_commands.Clear();
_commands.SetRenderTarget(_raymarchTexture);
_commands.SetViewport(new Rect(0f, 0f, _raymarchTexture.width, _raymarchTexture.height));
_commands.ClearRenderTarget(false, true, Color.clear);
_commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1);
commands.SetRenderTarget(_raymarchTexture);
commands.SetViewport(new Rect(0f, 0f, _raymarchTexture.width, _raymarchTexture.height));
commands.ClearRenderTarget(false, true, Color.clear);
commands.DrawProcedural(Matrix4x4.identity, _material!, raymarchPass, MeshTopology.Triangles, 3, 1);
if (useTemporalHistory)
{
_commands.SetRenderTarget(finalCloudTexture);
_commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height));
_commands.ClearRenderTarget(false, true, Color.clear);
_commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1);
commands.SetRenderTarget(finalCloudTexture);
commands.SetViewport(new Rect(0f, 0f, finalCloudTexture.width, finalCloudTexture.height));
commands.ClearRenderTarget(false, true, Color.clear);
commands.DrawProcedural(Matrix4x4.identity, _material!, temporalPass, MeshTopology.Triangles, 3, 1);
}
_commands.SetRenderTarget(BuiltinRenderTextureType.CameraTarget);
_commands.SetViewport(camera.pixelRect);
_commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1);
Graphics.ExecuteCommandBuffer(_commands);
if (depthTarget.IsValid())
commands.SetRenderTarget(colorTarget, depthTarget);
else
commands.SetRenderTarget(colorTarget);
commands.SetViewport(camera.pixelRect);
commands.DrawProcedural(Matrix4x4.identity, _material!, compositePass, MeshTopology.Triangles, 3, 1);
if (useTemporalHistory)
{
_historyTextureIndex = historyWriteIndex;
@ -262,7 +278,6 @@ namespace GameCult.Eve.UnityScene.Fields
private void ReleaseRenderState()
{
if (_commands != null) { _commands.Release(); _commands = null; }
ReleaseRenderTexture(ref _raymarchTexture);
ReleaseRenderTexture(ref _historyTextures[0]);
ReleaseRenderTexture(ref _historyTextures[1]);
@ -276,6 +291,51 @@ namespace GameCult.Eve.UnityScene.Fields
ResetTemporalHistory();
}
private sealed class EveUnityFieldsVolumePass : ScriptableRenderPass
{
private readonly EveUnityFieldsVolumeRenderer _owner;
private sealed class PassData
{
internal Camera Camera = null!;
internal EveUnityFieldsVolumeRenderer Owner = null!;
internal TextureHandle ColorTarget;
internal TextureHandle DepthTarget;
}
internal EveUnityFieldsVolumePass(EveUnityFieldsVolumeRenderer 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 resourceData = frameData.Get<UniversalResourceData>();
var colorTarget = resourceData.activeColorTexture;
if (!colorTarget.IsValid()) return;
using var builder = renderGraph.AddUnsafePass<PassData>("Eve Fields Volume", out var passData);
passData.Camera = cameraData.camera;
passData.Owner = _owner;
passData.ColorTarget = colorTarget;
passData.DepthTarget = resourceData.activeDepthTexture;
builder.UseTexture(passData.ColorTarget, AccessFlags.ReadWrite);
if (passData.DepthTarget.IsValid())
builder.UseTexture(passData.DepthTarget, AccessFlags.Read);
builder.AllowPassCulling(false);
builder.SetRenderFunc(static (PassData data, UnsafeGraphContext context) =>
data.Owner.RenderVolume(context.cmd, data.Camera, data.ColorTarget, data.DepthTarget));
}
}
private void ResetTemporalHistory()
{
_historyTextureIndex = -1;

View file

@ -6,6 +6,7 @@ using GameCult.Eve.PluginFields;
using GameCult.Eve.UnityScene.Fields;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.Rendering.Universal;
namespace GameCult.Eve.UnityScene.Tests
{
@ -41,6 +42,14 @@ namespace GameCult.Eve.UnityScene.Tests
Assert.That(typeof(IEveUnityFieldsSplatsDocumentSource).GetEvent("FieldsSplatsAvailable"), Is.Not.Null);
}
[Test]
public void VolumeRendererCompositesBeforeProviderPostProcessing()
{
Assert.That(
EveUnityFieldsVolumeRenderer.CompositionRenderPassEvent,
Is.EqualTo(RenderPassEvent.BeforeRenderingPostProcessing));
}
[Test]
public void VolumeProgramAcceptsPortableTwoPassLifecycle()
{

View file

@ -1,7 +1,7 @@
{
"name": "org.gamecult.eve.unity-scene",
"displayName": "Eve Unity Scene Lowering",
"version": "0.3.45",
"version": "0.3.46",
"unity": "6000.0",
"description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.",
"dependencies": {