diff --git a/packages/org.gamecult.eve.unity-scene/README.md b/packages/org.gamecult.eve.unity-scene/README.md index f7d3d74..9150c15 100644 --- a/packages/org.gamecult.eve.unity-scene/README.md +++ b/packages/org.gamecult.eve.unity-scene/README.md @@ -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. 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 add27f9..7848eb5 100644 --- a/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs +++ b/packages/org.gamecult.eve.unity-scene/Runtime/Fields/EveUnityFieldsVolumeRenderer.cs @@ -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(); + if (cameraData.camera == null || _owner._host == null || + _owner._host.ActiveCameraTransform != cameraData.camera.transform) + return; + + var resourceData = frameData.Get(); + var colorTarget = resourceData.activeColorTexture; + if (!colorTarget.IsValid()) return; + + using var builder = renderGraph.AddUnsafePass("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; 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 7be1fc4..a2b5398 100644 --- a/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs +++ b/packages/org.gamecult.eve.unity-scene/Tests/Editor/EveFieldsUnitySceneProjectionTests.cs @@ -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() { diff --git a/packages/org.gamecult.eve.unity-scene/package.json b/packages/org.gamecult.eve.unity-scene/package.json index 5822ed0..e4f7449 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.45", + "version": "0.3.46", "unity": "6000.0", "description": "Provider-agnostic Unity scene shell for gamecult.eve.surface.v1 interactive world surfaces.", "dependencies": {