using Sandbox.Volumes; namespace Sandbox; /// /// Manages post-processing effects for cameras and volumes within a scene, handling their application during rendering /// and editor preview stages. /// /// This system coordinates the collection and application of post-process effects based on camera /// position and active post-process volumes. In editor mode, it supports previewing effects for selected volumes or /// cameras. Implements both scene stage and render thread interfaces to integrate with the rendering /// pipeline. public sealed partial class PostProcessSystem : GameObjectSystem, Component.ISceneStage, Component.IRenderThread { [ConVar( "r_postprocess", ConVarFlags.Saved, Help = "Enable or disable post process effects." )] internal static bool EnablePostProcess { get; set; } = true; public PostProcessSystem( Scene scene ) : base( scene ) { } /// /// Called at the very end of the scene update, after all other components have been ticked. /// We use it to update our post processing effects for each camera. /// void Component.ISceneStage.End() { if ( !EnablePostProcess ) return; // // Editor behavior is special // if ( Scene.IsEditor ) { UpdateEditorScene(); return; } foreach ( var cc in Scene.GetAll() ) { UpdateCamera( cc ); } } void UpdateEditorScene() { if ( Scene.Camera is null ) return; Scene.Camera.PostProcess.Clear(); Scene.Camera.AutoExposure.Enabled = true; Scene.Camera.AutoExposure.Compensation = 0; Scene.Camera.AutoExposure.Rate = 20; Scene.Camera.AutoExposure.MinimumExposure = 1; Scene.Camera.AutoExposure.MaximumExposure = 2; // // If we have an object selected // if ( Scene.Editor?.SelectedGameObject is GameObject go ) { // // And it's a camera // if ( go.GetComponentInParent( false, true ) is CameraComponent cc ) { UpdateCamera( cc ); return; } // // Or if it's a volume // if ( go.GetComponentInParent( false, true ) is PostProcessVolume volume && volume.EditorPreview ) { PreviewVolume( volume ); return; } } // // By default just update the main camera // if ( Scene.Camera is CameraComponent mainCamera ) { UpdateCamera( mainCamera ); } } private void UpdateCamera( CameraComponent cc ) { cc.PostProcess.Clear(); if ( !cc.EnablePostProcessing ) return; var pos = cc.PostProcessAnchor.IsValid() ? cc.PostProcessAnchor.WorldPosition : cc.WorldPosition; List effects = cc.GetComponentsInChildren() .Select( x => new WeightedEffect { Effect = x, Weight = 1 } ) .ToList(); var volumes = Scene.GetSystem()?.FindAll( pos ); foreach ( var volume in volumes.OrderBy( x => x.Priority ) ) { var weight = volume.GetWeight( pos ); effects.AddRange( volume.GetComponentsInChildren().Select( x => new WeightedEffect { Effect = x, Weight = weight } ) ); } foreach ( var group in effects.GroupBy( x => x.Effect.GetType() ) ) { var effect = group.First(); var ctx = new PostProcessContext() { Camera = cc, Components = group.ToArray() }; effect.Effect.Build( ctx ); } } /// /// Called in editor mode, when a volume is selected /// private void PreviewVolume( PostProcessVolume volume ) { var data = Scene.Camera.PostProcess; var pos = volume.WorldPosition; List effects = volume.GetComponentsInChildren().Select( x => new WeightedEffect { Effect = x, Weight = 1 } ).ToList(); foreach ( var group in effects.GroupBy( x => x.Effect.GetType() ) ) { var effect = group.First(); var ctx = new PostProcessContext() { Camera = Scene.Camera, Components = group.ToArray() }; effect.Effect.Build( ctx ); } } /// /// Called whenever a camera is rendering a specific stage. This is called on the render thread. /// void Component.IRenderThread.OnRenderStage( CameraComponent camera, Sandbox.Rendering.Stage stage ) { if ( !EnablePostProcess ) return; if ( Graphics.SceneView.GetPostProcessEnabled() == false ) return; // Don't run explicit post process effects if we're in ToolsVis, other command lists like SSR/SSAO should still run if ( Graphics.SceneView.GetToolsVisMode() != (int)SceneCameraDebugMode.Normal && stage >= Rendering.Stage.BeforePostProcess && stage <= Rendering.Stage.AfterPostProcess ) return; camera.PostProcess.OnRenderStage( stage ); } }