using Sandbox.Volumes; using System.Text.Json.Nodes; namespace SceneTests.Components; /// /// Headless coverage for the post-processing component state that doesn't need /// a renderer: PostProcessVolume's blending math and VolumeSystem lookups, plus /// the two effects whose types are free of static shader/material initializers /// (MotionBlur, BlitOverlay) and the pure HighlightOutline marker component. /// The other effects (Bloom, Vignette, Sharpen, ...) hold static /// Material.FromShader fields, so even instantiating them risks loading /// resources without a booted engine - they are covered in the integration tier. /// [TestClass] [DoNotParallelize] public class PostProcessStateTest : SceneTest { /// /// PostProcessVolume's defaults match the source initializers: priority 0, /// full blend weight, a 50 unit blend distance, editor preview on, and the /// VolumeComponent default of a 100 unit box volume. /// [TestMethod] public void VolumeDefaultsMatchSource() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var volume = go.Components.Create(); Assert.AreEqual( 0, volume.Priority ); Assert.AreEqual( 1.0f, volume.BlendWeight ); Assert.AreEqual( 50.0f, volume.BlendDistance ); Assert.IsTrue( volume.EditorPreview ); Assert.AreEqual( SceneVolume.VolumeTypes.Box, volume.SceneVolume.Type ); Assert.AreEqual( BBox.FromPositionAndSize( 0, 100 ), volume.SceneVolume.Box ); Assert.IsFalse( volume.IsInfinite ); } /// /// GetWeight remaps the distance to the volume's edge into 0..BlendWeight /// over BlendDistance: at the center of the default box (50 units from every /// face, exactly the default BlendDistance) it returns the full blend weight, /// on the surface it returns 0, and an infinite volume always returns the /// blend weight no matter the position. /// [TestMethod] public void VolumeWeightBlending() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var volume = go.Components.Create(); Assert.AreEqual( 1.0f, volume.GetWeight( Vector3.Zero ), 0.0001f ); Assert.AreEqual( 0.0f, volume.GetWeight( new Vector3( 0, 0, 50 ) ), 0.0001f ); volume.BlendWeight = 0.5f; Assert.AreEqual( 0.5f, volume.GetWeight( Vector3.Zero ), 0.0001f ); volume.SceneVolume = new SceneVolume { Type = SceneVolume.VolumeTypes.Infinite }; Assert.IsTrue( volume.IsInfinite ); Assert.AreEqual( 0.5f, volume.GetWeight( new Vector3( 99999, 0, 0 ) ), 0.0001f ); } /// /// The scene VolumeSystem finds an enabled PostProcessVolume by position - /// inside the default box but not outside it - and stops finding it once the /// component is disabled. Ticking a camera-less scene with the volume enabled /// is safe because PostProcessSystem only acts on cameras. /// [TestMethod] public void VolumeSystemLookup() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var volume = go.Components.Create(); Assert.AreSame( volume, scene.Volumes.FindSingle( Vector3.Zero ) ); Assert.IsNull( scene.Volumes.FindSingle( new Vector3( 500, 0, 0 ) ) ); volume.Enabled = false; Assert.IsNull( scene.Volumes.FindSingle( Vector3.Zero ) ); volume.Enabled = true; Assert.AreSame( volume, scene.Volumes.FindSingle( Vector3.Zero ) ); for ( int i = 0; i < 10; i++ ) scene.GameTick(); Assert.IsTrue( volume.IsValid ); } /// /// PostProcessVolume's blending configuration and volume shape survive the /// component serialize/deserialize round trip into a new scene. /// [TestMethod] public void VolumeRoundTrip() { JsonObject json; var sceneA = new Scene(); using ( sceneA.Push() ) { var go = sceneA.CreateObject(); var volume = go.Components.Create(); volume.Priority = 3; volume.BlendWeight = 0.25f; volume.BlendDistance = 100.0f; volume.EditorPreview = false; volume.SceneVolume = new SceneVolume { Type = SceneVolume.VolumeTypes.Sphere, Sphere = new Sphere( 0, 200 ) }; json = (JsonObject)volume.Serialize(); } var sceneB = new Scene(); using ( sceneB.Push() ) { var go = sceneB.CreateObject(); var copy = go.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.AreEqual( 3, copy.Priority ); Assert.AreEqual( 0.25f, copy.BlendWeight ); Assert.AreEqual( 100.0f, copy.BlendDistance ); Assert.IsFalse( copy.EditorPreview ); Assert.AreEqual( SceneVolume.VolumeTypes.Sphere, copy.SceneVolume.Type ); Assert.AreEqual( 200.0f, copy.SceneVolume.Sphere.Radius ); } } /// /// MotionBlur is one of the few effects without static resource fields, so /// it can exist headless: the Scale default matches the source, and a /// non-default value survives the round trip into a new scene where the /// copy comes back enabled. /// [TestMethod] public void MotionBlurDefaultsAndRoundTrip() { JsonObject json; var sceneA = new Scene(); using ( sceneA.Push() ) { var go = sceneA.CreateObject(); var blur = go.Components.Create(); Assert.AreEqual( 0.05f, blur.Scale ); blur.Scale = 0.4f; json = (JsonObject)blur.Serialize(); } var sceneB = new Scene(); using ( sceneB.Push() ) { var go = sceneB.CreateObject(); var copy = go.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.IsTrue( copy.Enabled ); Assert.AreEqual( 0.4f, copy.Scale ); } } /// /// BlitOverlay's defaults match the source initializers (a 0.1 blend, normal /// blend mode, no material), and its blend configuration survives the round /// trip into a new scene with the material reference staying null. /// [TestMethod] public void BlitOverlayDefaultsAndRoundTrip() { JsonObject json; var sceneA = new Scene(); using ( sceneA.Push() ) { var go = sceneA.CreateObject(); var overlay = go.Components.Create(); Assert.AreEqual( 0.1f, overlay.Blend ); Assert.AreEqual( BlendMode.Normal, overlay.BlendMode ); Assert.IsNull( overlay.Material ); Assert.AreEqual( 0, overlay.Order ); overlay.Blend = 0.9f; overlay.BlendMode = BlendMode.Multiply; overlay.Order = 7; json = (JsonObject)overlay.Serialize(); } var sceneB = new Scene(); using ( sceneB.Push() ) { var go = sceneB.CreateObject(); var copy = go.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.AreEqual( 0.9f, copy.Blend ); Assert.AreEqual( BlendMode.Multiply, copy.BlendMode ); Assert.AreEqual( 7, copy.Order ); Assert.IsNull( copy.Material ); } } /// /// Effects register in the scene's object index under their BasePostProcess /// base while enabled, so the family lookups see them: the scene-wide /// enabled-only query, the FindMode-based component query that also includes /// disabled ones, and ticking a camera-less scene leaves them untouched. /// [TestMethod] public void EffectFamilyLookup() { var scene = new Scene(); using var sceneScope = scene.Push(); var go = scene.CreateObject(); var blur = go.Components.Create(); var overlay = go.Components.Create(); Assert.AreEqual( 2, scene.GetAllComponents().Count() ); Assert.AreEqual( 2, go.Components.GetAll( FindMode.EverythingInSelf ).Count() ); overlay.Enabled = false; Assert.AreSame( blur, scene.GetAllComponents().Single() ); Assert.AreEqual( 2, go.Components.GetAll( FindMode.EverythingInSelf ).Count() ); overlay.Enabled = true; for ( int i = 0; i < 10; i++ ) scene.GameTick(); Assert.IsTrue( blur.IsValid ); Assert.IsTrue( overlay.IsValid ); Assert.AreEqual( 2, scene.GetAllComponents().Count() ); } /// /// HighlightOutline is a pure marker component for the Highlight effect: its /// color and width defaults match the source, GetOutlineTargets is empty both /// with manual override (null target list) and in automatic mode when no /// renderers exist, and its configuration survives a round trip. /// [TestMethod] public void HighlightOutlineStateAndTargets() { JsonObject json; var sceneA = new Scene(); using ( sceneA.Push() ) { var go = sceneA.CreateObject(); var outline = go.Components.Create(); Assert.AreEqual( Color.White, outline.Color ); Assert.AreEqual( Color.Black * 0.4f, outline.ObscuredColor ); Assert.AreEqual( Color.Transparent, outline.InsideColor ); Assert.AreEqual( Color.Transparent, outline.InsideObscuredColor ); Assert.AreEqual( 0.25f, outline.Width ); Assert.IsFalse( outline.OverrideTargets ); Assert.IsNull( outline.Material ); // Automatic mode with no renderers in the hierarchy Assert.AreEqual( 0, outline.GetOutlineTargets().Count() ); // Manual mode with no target list assigned outline.OverrideTargets = true; Assert.AreEqual( 0, outline.GetOutlineTargets().Count() ); outline.Width = 0.5f; outline.Color = new Color( 1.0f, 0.5f, 0.0f, 1.0f ); json = (JsonObject)outline.Serialize(); } var sceneB = new Scene(); using ( sceneB.Push() ) { var go = sceneB.CreateObject(); var copy = go.Components.Create( false ); copy.DeserializeImmediately( json ); Assert.AreEqual( 0.5f, copy.Width ); Assert.AreEqual( new Color( 1.0f, 0.5f, 0.0f, 1.0f ), copy.Color ); Assert.IsTrue( copy.OverrideTargets ); } } }