From bd0d021ab56ecc2e5ba6dae719d5619d2f241bbf Mon Sep 17 00:00:00 2001 From: CloudeaSoft Date: Mon, 19 Oct 2026 21:58:22 +0800 Subject: [PATCH 1/2] fix(yggdrasil): correct King Jelly Ball reflections and add surface ripples --- .../VFX/Pipelines/ScreenReflectionPipeline.cs | 26 ++- .../KingJellyBall/KingJellyBall_Reflection.cs | 35 ++-- .../KingJellyBall_ReflectionPipeline.cs | 29 +++ .../VFXs/KingJellyBall_SurfaceReflection.fx | 89 ++++++++ Tools/Tests/KingJellyBallReflectionProbe.cs | 190 ++++++++++++++++++ Tools/Tests/Test-KingJellyBallReflection.ps1 | 39 ++++ 6 files changed, 378 insertions(+), 30 deletions(-) create mode 100644 Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_ReflectionPipeline.cs create mode 100644 Sources/Modules/Yggdrasil/YggdrasilTown/VFXs/KingJellyBall_SurfaceReflection.fx create mode 100644 Tools/Tests/KingJellyBallReflectionProbe.cs create mode 100644 Tools/Tests/Test-KingJellyBallReflection.ps1 diff --git a/Sources/Everglow.Function/VFX/Pipelines/ScreenReflectionPipeline.cs b/Sources/Everglow.Function/VFX/Pipelines/ScreenReflectionPipeline.cs index d79722ac1..b6a0e0901 100644 --- a/Sources/Everglow.Function/VFX/Pipelines/ScreenReflectionPipeline.cs +++ b/Sources/Everglow.Function/VFX/Pipelines/ScreenReflectionPipeline.cs @@ -11,6 +11,10 @@ public class ScreenReflectionPipeline : Pipeline { private RenderTarget2D screenReflectionScreen; // 反射区域 + protected virtual bool IncludeLocalPlayer => true; + + protected virtual Matrix CaptureTransform => Matrix.Invert(Main.GameViewMatrix.TransformationMatrix); + public override void Load() { Ins.MainThread.AddTask(() => @@ -43,16 +47,17 @@ public override void BeginRender() RasterizerState.CullNone); spriteBatch.Draw(Main.screenTarget, Vector2.Zero, Color.White); spriteBatch.End(); - List players = new List() { Main.LocalPlayer }; - - // 外加玩家 - Main.PlayerRenderer.DrawPlayers(Main.Camera, players); + if (IncludeLocalPlayer) + { + List players = new List() { Main.LocalPlayer }; + Main.PlayerRenderer.DrawPlayers(Main.Camera, players); + } graphicsDevice.SetRenderTarget(screenReflectionScreen); // 以另一种方法绘制保存下来的屏幕 spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Opaque, SamplerState.PointClamp, DepthStencilState.Default, - RasterizerState.CullNone, null, Matrix.Invert(Main.GameViewMatrix.TransformationMatrix)); + RasterizerState.CullNone, null, CaptureTransform); spriteBatch.Draw(Main.screenTargetSwap, Vector2.Zero, Color.White); spriteBatch.End(); @@ -67,6 +72,14 @@ public override void BeginRender() spriteBatch.Begin(SpriteSortMode.Immediate, BlendState.Additive, SamplerState.PointClamp, DepthStencilState.None, RasterizerState.CullNone); + ApplyReflectionEffect(); + Main.graphics.GraphicsDevice.Textures[0] = screenReflectionScreen; + Main.graphics.GraphicsDevice.SamplerStates[0] = SamplerState.PointWrap; + Main.graphics.GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp; + } + + protected virtual void ApplyReflectionEffect() + { Effect ef = Commons.ModAsset.ScreenReflection_NormalColor.Value; var viewport = Main.graphics.GraphicsDevice.Viewport; @@ -85,9 +98,6 @@ public override void BeginRender() ef.Parameters["uViewProj"].SetValue(Main.GameViewMatrix.TransformationMatrix * projection); ef.CurrentTechnique.Passes["Test"].Apply(); - Main.graphics.GraphicsDevice.Textures[0] = screenReflectionScreen; - Main.graphics.GraphicsDevice.SamplerStates[0] = SamplerState.PointWrap; - Main.graphics.GraphicsDevice.SamplerStates[1] = SamplerState.PointClamp; } public override void Render(IEnumerable visuals) diff --git a/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_Reflection.cs b/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_Reflection.cs index 20426b54f..5371577aa 100644 --- a/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_Reflection.cs +++ b/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_Reflection.cs @@ -2,10 +2,10 @@ namespace Everglow.Yggdrasil.YggdrasilTown.NPCs.KingJellyBall; -[Pipeline(typeof(ScreenReflectionPipeline))] +[Pipeline(typeof(KingJellyBall_ReflectionPipeline))] public class KingJellyBall_Reflection : Visual { - public override CodeLayer DrawLayer => CodeLayer.PostDrawProjectiles; + public override CodeLayer DrawLayer => CodeLayer.PreDrawFilter; public NPC MyKingJellyBallOwner; @@ -51,20 +51,24 @@ public override void Draw() Active = false; return; } - - var textureOld = Main.graphics.GraphicsDevice.Textures[1]; - Main.graphics.GraphicsDevice.Textures[1] = ModAsset.KingJellyBall_Reflect.Value; float timeValue = (float)Main.time * 0.03f; List jellyBallBodyInner = new List(); // adjust center base on the polar funtion graph. Vector2 offset = new Vector2(0, -60) + new Vector2(0, -120 * (Scale - 0.3f)); Vector2 offsetedCenter = Position + offset; + Effect reflection = ModAsset.KingJellyBall_SurfaceReflection.Value; + float bodyAsymmetry = 110 + 10 * MathF.Sin(timeValue); + reflection.Parameters["uSurfaceCenter"].SetValue(offsetedCenter + new Vector2(0, 1.3f * bodyAsymmetry * Scale) - Main.screenPosition); + reflection.Parameters["uSurfaceScale"].SetValue(Scale); + reflection.Parameters["uTime"].SetValue(Main.GlobalTimeWrappedHourly); + reflection.Parameters["uRippleStrength"].SetValue(0.65f); + reflection.CurrentTechnique.Passes["Test"].Apply(); int step = 150; for (int theta = 0; theta <= step; theta++) { float a = 200; - float b = 110 + 10 * MathF.Sin(timeValue); + float b = bodyAsymmetry; float angle = theta / (float)step * MathHelper.TwoPi; float r = a - b * MathF.Sin(angle); @@ -79,22 +83,9 @@ public override void Draw() Vector2 toDistance = new Vector2(-r, 0).RotatedBy(angle); toDistance.Y *= 1.3f; Vector2 width = Vector2.Normalize(toDistance) * 2f; - jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(offsetedCenter, -150), new Color(0.4f, 0.6f, 1f, 0.5f), new Vector3(0.5f, 0.5f, 0))); - jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(offsetedCenter + toDistance - width, -10), new Color(0.7f, 0.7f, 1f, 1f), new Vector3(new Vector2(0.5f) + new Vector2(-0.27f, 0).RotatedBy(angle), 0))); - } - if (jellyBallBodyInner.Count >= 2) - { - Main.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, jellyBallBodyInner.ToArray(), 0, jellyBallBodyInner.Count - 2); - } - else - { - jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(0), new Color(0.5f, 0.5f, 0.5f), new Vector3(0.5f, 0.5f, 0))); - jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(0), new Color(0.5f, 0.5f, 0.5f), new Vector3(0.5f, 0.5f, 0))); - jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(0), new Color(0.5f, 0.5f, 0.5f), new Vector3(0.5f, 0.5f, 0))); - jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(0), new Color(0.5f, 0.5f, 0.5f), new Vector3(0.5f, 0.5f, 0))); - - Main.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, jellyBallBodyInner.ToArray(), 0, jellyBallBodyInner.Count - 2); + jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(offsetedCenter, -1), new Color(0.4f, 0.6f, 1f, 0.5f), new Vector3(0.5f, 0.5f, 0))); + jellyBallBodyInner.Add(new MirrorFaceVertex(new Vector3(offsetedCenter + toDistance - width, -1), new Color(0.7f, 0.7f, 1f, 1f), new Vector3(new Vector2(0.5f) + new Vector2(-0.27f, 0).RotatedBy(angle), 0))); } - Main.graphics.GraphicsDevice.Textures[1] = textureOld; + Main.graphics.GraphicsDevice.DrawUserPrimitives(PrimitiveType.TriangleStrip, jellyBallBodyInner.ToArray(), 0, jellyBallBodyInner.Count - 2); } } diff --git a/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_ReflectionPipeline.cs b/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_ReflectionPipeline.cs new file mode 100644 index 000000000..edea6e713 --- /dev/null +++ b/Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_ReflectionPipeline.cs @@ -0,0 +1,29 @@ +namespace Everglow.Yggdrasil.YggdrasilTown.NPCs.KingJellyBall; + +public class KingJellyBall_ReflectionPipeline : ScreenReflectionPipeline +{ + // PreDrawFilter captures the completed scene, including all players. + protected override bool IncludeLocalPlayer => false; + + protected override Matrix CaptureTransform => Matrix.Identity; + + public override void BeginRender() + { + base.BeginRender(); + Main.graphics.GraphicsDevice.SamplerStates[0] = SamplerState.LinearClamp; + } + + protected override void ApplyReflectionEffect() + { + Effect reflection = ModAsset.KingJellyBall_SurfaceReflection.Value; + var viewport = Main.graphics.GraphicsDevice.Viewport; + var projection = Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 500); + reflection.Parameters["uModel"].SetValue(Matrix.CreateTranslation(new Vector3(-Main.screenPosition, 0))); + reflection.Parameters["uViewProj"].SetValue(Main.GameViewMatrix.TransformationMatrix * projection); + reflection.Parameters["uScreenTransform"].SetValue(Main.GameViewMatrix.TransformationMatrix); + reflection.Parameters["uViewportSize"].SetValue(new Vector2(viewport.Width, viewport.Height)); + reflection.Parameters["uFresnelF0"].SetValue(new Vector3(0.17f)); + reflection.Parameters["uKs"].SetValue(Vector3.One); + reflection.CurrentTechnique.Passes["Test"].Apply(); + } +} diff --git a/Sources/Modules/Yggdrasil/YggdrasilTown/VFXs/KingJellyBall_SurfaceReflection.fx b/Sources/Modules/Yggdrasil/YggdrasilTown/VFXs/KingJellyBall_SurfaceReflection.fx new file mode 100644 index 000000000..333e6f57f --- /dev/null +++ b/Sources/Modules/Yggdrasil/YggdrasilTown/VFXs/KingJellyBall_SurfaceReflection.fx @@ -0,0 +1,89 @@ +sampler uScreenBuffer : register(s0); + +float3 uFresnelF0; +float3 uKs; +float2 uViewportSize; +float4x4 uModel; +float4x4 uViewProj; +float4x4 uScreenTransform; +float2 uSurfaceCenter; +float uSurfaceScale; +float uTime; +float uRippleStrength; + +struct VSInput +{ + float3 Pos : POSITION0; + float4 Color : COLOR0; + float3 Texcoord : TEXCOORD0; +}; + +struct PSInput +{ + float4 Pos : SV_POSITION; + float4 Color : COLOR0; + float2 SurfacePosition : TEXCOORD0; +}; + +PSInput VertexShaderFunction(VSInput input) +{ + PSInput output; + float4 position = mul(float4(input.Pos, 1), uModel); + output.Pos = mul(position, uViewProj); + output.SurfacePosition = position.xy; + output.Color = input.Color; + return output; +} + +float4 PixelShaderFunction(PSInput input) : COLOR0 +{ + float scale = max(uSurfaceScale, 0.01); + float2 localPosition = input.SurfacePosition - uSurfaceCenter; + float2 radii = float2(260, 280) * scale; + float2 surface = localPosition / radii; + float radiusSquared = dot(surface, surface); + if (radiusSquared >= 1) + return 0; + + // A convex ellipsoidal cap facing the camera. The source plane is z = 0; + // the cap is closest at its center. Depth and normal describe the same surface. + float cap = sqrt(max(1 - radiusSquared, 0.0025)); + float depth = scale * (12 + 70 * (1 - cap)); + float2 gradient = 70 * scale * surface / (radii * cap); + + // Smooth traveling waves perturb both height and its analytic derivatives. + // Coordinates scale with the body, so shrinking cannot amplify the distortion. + float2 wavePosition = localPosition / scale; + float2 frequency0 = float2(0.035, 0.022); + float2 frequency1 = float2(-0.018, 0.041); + float phase0 = dot(wavePosition, frequency0) - uTime * 1.8; + float phase1 = dot(wavePosition, frequency1) + uTime * 1.3; + depth += scale * uRippleStrength * (0.65 * sin(phase0) + 0.35 * sin(phase1)); + gradient += uRippleStrength * (0.65 * cos(phase0) * frequency0 + 0.35 * cos(phase1) * frequency1); + + // All positions and derivatives use screen axes: positive Y points down. + float3 normal = normalize(float3(gradient, 1)); + float3 reflected = reflect(float3(0, 0, -1), normal); + float visibility = smoothstep(0.12, 0.3, reflected.z); + float2 sourcePosition = input.SurfacePosition + depth * reflected.xy / max(reflected.z, 0.12); + + // Keep the captured image in screen space; transform the intersection once. + float2 screenPosition = mul(float4(sourcePosition, 0, 1), uScreenTransform).xy; + float2 edgeDistance = min(screenPosition, uViewportSize - screenPosition); + visibility *= smoothstep(0, 8, min(edgeDistance.x, edgeDistance.y)); + float2 uv = clamp(screenPosition / uViewportSize, 0.5 / uViewportSize, 1 - 0.5 / uViewportSize); + + float3 fresnel = uFresnelF0 + (1 - uFresnelF0) * pow(saturate(1 - normal.z), 5); + float3 scene = pow(max(tex2D(uScreenBuffer, uv).rgb, 0), 2.2); + float3 color = pow(max(scene * uKs * fresnel, 0), 1 / 2.2); + return float4(color, 1) * input.Color * visibility; +} + +technique Technique1 +{ + pass Test + { + VertexShader = compile vs_3_0 VertexShaderFunction(); + PixelShader = compile ps_3_0 PixelShaderFunction(); + } +} diff --git a/Tools/Tests/KingJellyBallReflectionProbe.cs b/Tools/Tests/KingJellyBallReflectionProbe.cs new file mode 100644 index 000000000..15f0a389c --- /dev/null +++ b/Tools/Tests/KingJellyBallReflectionProbe.cs @@ -0,0 +1,190 @@ +using Microsoft.Xna.Framework; +using Microsoft.Xna.Framework.Content; +using Microsoft.Xna.Framework.Graphics; + +namespace Everglow.Tools.Tests; + +// Standalone GPU probe: deliberately does not load Terraria.Main or the game world. +internal static class KingJellyBallReflectionProbe +{ + public static int Main(string[] args) + { + using var probe = new ReflectionProbe(args[0], args[1], args.Contains("--legacy")); + probe.RunOneFrame(); + return probe.Failures == 0 ? 0 : 1; + } + + private sealed class ReflectionProbe : Game + { + private const int Size = 1024; + private readonly string root; + private readonly string configuration; + private readonly bool legacy; + private readonly GraphicsDeviceManager manager; + + public ReflectionProbe(string root, string configuration, bool legacy) + { + this.root = root; + this.configuration = configuration; + this.legacy = legacy; + manager = new GraphicsDeviceManager(this) + { + GraphicsProfile = GraphicsProfile.HiDef, + PreferredBackBufferWidth = Size, + PreferredBackBufferHeight = Size, + }; + } + + public int Failures { get; private set; } + + protected override void Initialize() + { + base.Initialize(); + string project = legacy ? "Sources/Everglow.Function" : "Sources/Modules/Yggdrasil"; + using var content = new ContentManager(Services, Path.Combine(root, project, "bin", configuration, "net8.0/Assets")); + Effect effect = content.Load(legacy + ? "VFX/Effects/ScreenReflection_NormalColor" + : "YggdrasilTown/VFXs/KingJellyBall_SurfaceReflection"); + using var file = File.OpenRead(Path.Combine(root, "Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_Reflect.png")); + using var normals = Texture2D.FromStream(GraphicsDevice, file); + using var source = new Texture2D(GraphicsDevice, Size, Size, false, SurfaceFormat.Vector4); + var ramp = new Vector4[Size * Size]; + for (int y = 0; y < Size; y++) + { + for (int x = 0; x < Size; x++) + { + // Encode source coordinates in color, then read the real shader's mapping back. + ramp[y * Size + x] = new Vector4((x + 0.5f) / Size, (y + 0.5f) / Size, 0.25f, 1); + } + } + source.SetData(ramp); + using var target = new RenderTarget2D(GraphicsDevice, Size, Size, false, SurfaceFormat.Vector4, DepthFormat.None); + foreach (float scale in new[] { 1f, 0.3f }) + { + Vector4[] plain = Render(effect, source, normals, target, scale, 0, 0); + int delta = (int)(20 * scale); + Vector4 left = Sample(plain, 512 - delta, 512); + Vector4 right = Sample(plain, 512 + delta, 512); + Vector4 top = Sample(plain, 512, 512 - delta); + Vector4 bottom = Sample(plain, 512, 512 + delta); + float width = 2 * delta / ((right.X - left.X) * Size); + float height = 2 * delta / ((bottom.Y - top.Y) * Size); + Check(width is > 0.8f and < 1.2f && height is > 0.8f and < 1.2f, + $"Center scale={scale}: width={width:F3}, height={height:F3} (both 0.8..1.2)"); + + float previous = float.NegativeInfinity; + bool monotonic = true; + for (int offset = (int)(-170 * scale); offset <= (int)(170 * scale); offset++) + { + float value = Sample(plain, 512, 512 + offset).Y; + monotonic &= float.IsFinite(value) && value + 0.000001f >= previous; + previous = value; + } + Check(monotonic, $"Vertical reflection does not fold, scale={scale}"); + Vector4 sideLeft = Sample(plain, 512 + (int)(160 * scale), 512); + Vector4 sideRight = Sample(plain, 512 + (int)(180 * scale), 512); + float sideWidth = 20 * scale / ((sideRight.X - sideLeft.X) * Size); + Check(sideWidth > 0 && sideWidth < width - 0.05f, $"Side compresses: {sideWidth:F3} vs center {width:F3}"); + + Vector4[] wave0 = Render(effect, source, normals, target, scale, 0, 0.65f); + Vector4[] wave1 = Render(effect, source, normals, target, scale, 1, 0.65f); + float movement = 0; + float distortion = 0; + bool smooth = true; + for (int y = -100; y <= 100; y += 5) + { + for (int x = -100; x <= 100; x += 5) + { + int px = 512 + (int)(x * scale); + int py = 512 + (int)(y * scale); + Vector2 original = Coordinates(Sample(plain, px, py)); + Vector2 first = Coordinates(Sample(wave0, px, py)); + Vector2 second = Coordinates(Sample(wave1, px, py)); + movement = Math.Max(movement, Vector2.Distance(first, second)); + distortion = Math.Max(distortion, Vector2.Distance(original, first)); + smooth &= Sample(wave0, px + 1, py).X > Sample(wave0, px, py).X + && Sample(wave0, px, py + 1).Y > Sample(wave0, px, py).Y; + } + } + Check(movement > 0.05f && distortion < 6 * scale, + $"Ripples animate and stay subtle: movement={movement:F3}px, distortion={distortion:F3}px"); + Check(smooth, $"Ripples preserve local image orientation, scale={scale}"); + } + + Vector4[] reference = Render(effect, source, normals, target, 1, 0, 0); + foreach (float zoom in new[] { 0.75f, 1.5f }) + { + Vector4[] zoomed = Render(effect, source, normals, target, 1, 0, 0, zoom); + Vector2 intersection = Coordinates(Sample(reference, 552, 532)); + Vector2 expected = new Vector2(512) + (intersection - new Vector2(512)) * zoom; + Vector2 actual = Coordinates(Sample(zoomed, 512 + (int)(40 * zoom), 512 + (int)(20 * zoom))); + Check(Vector2.Distance(expected, actual) < 1.5f, $"Zoom={zoom} transforms the intersection once"); + } + + Vector4[] edge = Render(effect, source, normals, target, 1, 0, 0, center: new Vector2(5, 512)); + Vector4 edgeSample = Sample(edge, 1, 512); + Check(edgeSample.W < 0.1f && edgeSample.X < 0.01f, "Screen edge fades without sampling the opposite edge"); + Vector4[] grazing = Render(effect, source, normals, target, 1, 0, 0, radius: 300); + Check(Sample(grazing, 770, 512).W < 0.01f, "Grazing and backward rays fade out"); + Console.WriteLine($"GPU regression failures: {Failures}"); + Exit(); + } + + private Vector4[] Render(Effect effect, Texture2D source, Texture2D normals, RenderTarget2D target, + float scale, float time, float strength, float zoom = 1, Vector2? center = null, float radius = 240) + { + Vector2 origin = center ?? new Vector2(512); + GraphicsDevice device = GraphicsDevice; + device.SetRenderTarget(target); + device.Clear(Color.Transparent); + device.BlendState = BlendState.Opaque; + device.DepthStencilState = DepthStencilState.None; + device.RasterizerState = RasterizerState.CullNone; + Matrix view = Matrix.CreateTranslation(-512, -512, 0) * Matrix.CreateScale(zoom, zoom, 1) * Matrix.CreateTranslation(512, 512, 0); + effect.Parameters["uModel"]?.SetValue(Matrix.Identity); + effect.Parameters["uMNormal"]?.SetValue(Matrix.Identity); + effect.Parameters["uViewProj"]?.SetValue(view * Matrix.CreateOrthographicOffCenter(0, Size, Size, 0, 0, 500)); + effect.Parameters["uViewportSize"]?.SetValue(new Vector2(Size)); + effect.Parameters["uScreenDistanceMultipler"]?.SetValue(Vector2.One); + effect.Parameters["uFresnelF0"]?.SetValue(Vector3.One); + effect.Parameters["uKs"]?.SetValue(Vector3.One); + effect.Parameters["uSurfaceCenter"]?.SetValue(origin); + effect.Parameters["uSurfaceScale"]?.SetValue(scale); + effect.Parameters["uTime"]?.SetValue(time); + effect.Parameters["uRippleStrength"]?.SetValue(strength); + effect.Parameters["uScreenTransform"]?.SetValue(view); + effect.CurrentTechnique.Passes[0].Apply(); + device.Textures[0] = source; + device.Textures[1] = normals; + device.SamplerStates[0] = legacy ? SamplerState.PointWrap : SamplerState.LinearClamp; + device.SamplerStates[1] = SamplerState.PointClamp; + var vertices = new List(); + for (int i = 0; i <= 150; i++) + { + float angle = i * MathHelper.TwoPi / 150; + var direction = new Vector2(MathF.Cos(angle), MathF.Sin(angle)); + vertices.Add(new VertexPositionColorTexture(new Vector3(origin, -150), Color.White, new Vector2(0.5f))); + vertices.Add(new VertexPositionColorTexture(new Vector3(origin + direction * radius * scale, -10), + Color.White, new Vector2(0.5f) + direction * 0.27f)); + } + device.DrawUserPrimitives(PrimitiveType.TriangleStrip, vertices.ToArray(), 0, vertices.Count - 2); + device.SetRenderTarget(null); + var data = new Vector4[Size * Size]; + target.GetData(data); + return data; + } + + private static Vector4 Sample(Vector4[] data, int x, int y) => data[y * Size + x]; + + private static Vector2 Coordinates(Vector4 color) => new Vector2(color.X, color.Y) * Size; + + private void Check(bool condition, string message) + { + Console.WriteLine((condition ? "PASS " : "FAIL ") + message); + if (!condition) + { + Failures++; + } + } + } +} diff --git a/Tools/Tests/Test-KingJellyBallReflection.ps1 b/Tools/Tests/Test-KingJellyBallReflection.ps1 new file mode 100644 index 000000000..e44966ee0 --- /dev/null +++ b/Tools/Tests/Test-KingJellyBallReflection.ps1 @@ -0,0 +1,39 @@ +# Run after dotnet build. Uses the installed tML FNA backend; does not load Terraria. +# -Legacy is a negative control: the previous shader should fail the shape tests. +param( + [Parameter(Mandatory = $true)] + [string]$TmlDirectory, + [string]$Configuration = 'Debug', + [switch]$Legacy +) + +$ErrorActionPreference = 'Stop' +$sourceRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path +$fna = @(Get-ChildItem -LiteralPath (Join-Path $TmlDirectory 'Libraries') -Filter 'FNA.dll' -Recurse) +if ($fna.Count -ne 1) { throw 'Expected one FNA.dll in the tModLoader Libraries directory.' } +$probeDirectory = Join-Path ([IO.Path]::GetTempPath()) ('EverglowJellyReflection-' + [Guid]::NewGuid().ToString('N')) +New-Item -ItemType Directory -Path $probeDirectory | Out-Null +Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'KingJellyBallReflectionProbe.cs') -Destination (Join-Path $probeDirectory 'Program.cs') +$fnaPath = [Security.SecurityElement]::Escape($fna[0].FullName) +$project = @" + + + Exe + net8.0 + enable + + $fnaPath + +"@ +[IO.File]::WriteAllText((Join-Path $probeDirectory 'Probe.csproj'), $project, [Text.UTF8Encoding]::new($false)) +$previousPath = $env:PATH +try { + $env:PATH = (Join-Path $TmlDirectory 'Libraries/Native/Windows') + [IO.Path]::PathSeparator + $previousPath + $probeArguments = @($sourceRoot, $Configuration) + if ($Legacy) { $probeArguments += '--legacy' } + dotnet run --project (Join-Path $probeDirectory 'Probe.csproj') -- @probeArguments + if ($LASTEXITCODE -ne 0) { throw "GPU reflection regression failed (exit $LASTEXITCODE)." } +} +finally { + $env:PATH = $previousPath +} From 8f5e7774f44be451cf4606542e88c9b42c7207d6 Mon Sep 17 00:00:00 2001 From: CloudeaSoft Date: Mon, 19 Oct 2026 22:19:31 +0800 Subject: [PATCH 2/2] chore: remove King Jelly Ball reflection GPU probes --- Tools/Tests/KingJellyBallReflectionProbe.cs | 190 ------------------- Tools/Tests/Test-KingJellyBallReflection.ps1 | 39 ---- 2 files changed, 229 deletions(-) delete mode 100644 Tools/Tests/KingJellyBallReflectionProbe.cs delete mode 100644 Tools/Tests/Test-KingJellyBallReflection.ps1 diff --git a/Tools/Tests/KingJellyBallReflectionProbe.cs b/Tools/Tests/KingJellyBallReflectionProbe.cs deleted file mode 100644 index 15f0a389c..000000000 --- a/Tools/Tests/KingJellyBallReflectionProbe.cs +++ /dev/null @@ -1,190 +0,0 @@ -using Microsoft.Xna.Framework; -using Microsoft.Xna.Framework.Content; -using Microsoft.Xna.Framework.Graphics; - -namespace Everglow.Tools.Tests; - -// Standalone GPU probe: deliberately does not load Terraria.Main or the game world. -internal static class KingJellyBallReflectionProbe -{ - public static int Main(string[] args) - { - using var probe = new ReflectionProbe(args[0], args[1], args.Contains("--legacy")); - probe.RunOneFrame(); - return probe.Failures == 0 ? 0 : 1; - } - - private sealed class ReflectionProbe : Game - { - private const int Size = 1024; - private readonly string root; - private readonly string configuration; - private readonly bool legacy; - private readonly GraphicsDeviceManager manager; - - public ReflectionProbe(string root, string configuration, bool legacy) - { - this.root = root; - this.configuration = configuration; - this.legacy = legacy; - manager = new GraphicsDeviceManager(this) - { - GraphicsProfile = GraphicsProfile.HiDef, - PreferredBackBufferWidth = Size, - PreferredBackBufferHeight = Size, - }; - } - - public int Failures { get; private set; } - - protected override void Initialize() - { - base.Initialize(); - string project = legacy ? "Sources/Everglow.Function" : "Sources/Modules/Yggdrasil"; - using var content = new ContentManager(Services, Path.Combine(root, project, "bin", configuration, "net8.0/Assets")); - Effect effect = content.Load(legacy - ? "VFX/Effects/ScreenReflection_NormalColor" - : "YggdrasilTown/VFXs/KingJellyBall_SurfaceReflection"); - using var file = File.OpenRead(Path.Combine(root, "Sources/Modules/Yggdrasil/YggdrasilTown/NPCs/KingJellyBall/KingJellyBall_Reflect.png")); - using var normals = Texture2D.FromStream(GraphicsDevice, file); - using var source = new Texture2D(GraphicsDevice, Size, Size, false, SurfaceFormat.Vector4); - var ramp = new Vector4[Size * Size]; - for (int y = 0; y < Size; y++) - { - for (int x = 0; x < Size; x++) - { - // Encode source coordinates in color, then read the real shader's mapping back. - ramp[y * Size + x] = new Vector4((x + 0.5f) / Size, (y + 0.5f) / Size, 0.25f, 1); - } - } - source.SetData(ramp); - using var target = new RenderTarget2D(GraphicsDevice, Size, Size, false, SurfaceFormat.Vector4, DepthFormat.None); - foreach (float scale in new[] { 1f, 0.3f }) - { - Vector4[] plain = Render(effect, source, normals, target, scale, 0, 0); - int delta = (int)(20 * scale); - Vector4 left = Sample(plain, 512 - delta, 512); - Vector4 right = Sample(plain, 512 + delta, 512); - Vector4 top = Sample(plain, 512, 512 - delta); - Vector4 bottom = Sample(plain, 512, 512 + delta); - float width = 2 * delta / ((right.X - left.X) * Size); - float height = 2 * delta / ((bottom.Y - top.Y) * Size); - Check(width is > 0.8f and < 1.2f && height is > 0.8f and < 1.2f, - $"Center scale={scale}: width={width:F3}, height={height:F3} (both 0.8..1.2)"); - - float previous = float.NegativeInfinity; - bool monotonic = true; - for (int offset = (int)(-170 * scale); offset <= (int)(170 * scale); offset++) - { - float value = Sample(plain, 512, 512 + offset).Y; - monotonic &= float.IsFinite(value) && value + 0.000001f >= previous; - previous = value; - } - Check(monotonic, $"Vertical reflection does not fold, scale={scale}"); - Vector4 sideLeft = Sample(plain, 512 + (int)(160 * scale), 512); - Vector4 sideRight = Sample(plain, 512 + (int)(180 * scale), 512); - float sideWidth = 20 * scale / ((sideRight.X - sideLeft.X) * Size); - Check(sideWidth > 0 && sideWidth < width - 0.05f, $"Side compresses: {sideWidth:F3} vs center {width:F3}"); - - Vector4[] wave0 = Render(effect, source, normals, target, scale, 0, 0.65f); - Vector4[] wave1 = Render(effect, source, normals, target, scale, 1, 0.65f); - float movement = 0; - float distortion = 0; - bool smooth = true; - for (int y = -100; y <= 100; y += 5) - { - for (int x = -100; x <= 100; x += 5) - { - int px = 512 + (int)(x * scale); - int py = 512 + (int)(y * scale); - Vector2 original = Coordinates(Sample(plain, px, py)); - Vector2 first = Coordinates(Sample(wave0, px, py)); - Vector2 second = Coordinates(Sample(wave1, px, py)); - movement = Math.Max(movement, Vector2.Distance(first, second)); - distortion = Math.Max(distortion, Vector2.Distance(original, first)); - smooth &= Sample(wave0, px + 1, py).X > Sample(wave0, px, py).X - && Sample(wave0, px, py + 1).Y > Sample(wave0, px, py).Y; - } - } - Check(movement > 0.05f && distortion < 6 * scale, - $"Ripples animate and stay subtle: movement={movement:F3}px, distortion={distortion:F3}px"); - Check(smooth, $"Ripples preserve local image orientation, scale={scale}"); - } - - Vector4[] reference = Render(effect, source, normals, target, 1, 0, 0); - foreach (float zoom in new[] { 0.75f, 1.5f }) - { - Vector4[] zoomed = Render(effect, source, normals, target, 1, 0, 0, zoom); - Vector2 intersection = Coordinates(Sample(reference, 552, 532)); - Vector2 expected = new Vector2(512) + (intersection - new Vector2(512)) * zoom; - Vector2 actual = Coordinates(Sample(zoomed, 512 + (int)(40 * zoom), 512 + (int)(20 * zoom))); - Check(Vector2.Distance(expected, actual) < 1.5f, $"Zoom={zoom} transforms the intersection once"); - } - - Vector4[] edge = Render(effect, source, normals, target, 1, 0, 0, center: new Vector2(5, 512)); - Vector4 edgeSample = Sample(edge, 1, 512); - Check(edgeSample.W < 0.1f && edgeSample.X < 0.01f, "Screen edge fades without sampling the opposite edge"); - Vector4[] grazing = Render(effect, source, normals, target, 1, 0, 0, radius: 300); - Check(Sample(grazing, 770, 512).W < 0.01f, "Grazing and backward rays fade out"); - Console.WriteLine($"GPU regression failures: {Failures}"); - Exit(); - } - - private Vector4[] Render(Effect effect, Texture2D source, Texture2D normals, RenderTarget2D target, - float scale, float time, float strength, float zoom = 1, Vector2? center = null, float radius = 240) - { - Vector2 origin = center ?? new Vector2(512); - GraphicsDevice device = GraphicsDevice; - device.SetRenderTarget(target); - device.Clear(Color.Transparent); - device.BlendState = BlendState.Opaque; - device.DepthStencilState = DepthStencilState.None; - device.RasterizerState = RasterizerState.CullNone; - Matrix view = Matrix.CreateTranslation(-512, -512, 0) * Matrix.CreateScale(zoom, zoom, 1) * Matrix.CreateTranslation(512, 512, 0); - effect.Parameters["uModel"]?.SetValue(Matrix.Identity); - effect.Parameters["uMNormal"]?.SetValue(Matrix.Identity); - effect.Parameters["uViewProj"]?.SetValue(view * Matrix.CreateOrthographicOffCenter(0, Size, Size, 0, 0, 500)); - effect.Parameters["uViewportSize"]?.SetValue(new Vector2(Size)); - effect.Parameters["uScreenDistanceMultipler"]?.SetValue(Vector2.One); - effect.Parameters["uFresnelF0"]?.SetValue(Vector3.One); - effect.Parameters["uKs"]?.SetValue(Vector3.One); - effect.Parameters["uSurfaceCenter"]?.SetValue(origin); - effect.Parameters["uSurfaceScale"]?.SetValue(scale); - effect.Parameters["uTime"]?.SetValue(time); - effect.Parameters["uRippleStrength"]?.SetValue(strength); - effect.Parameters["uScreenTransform"]?.SetValue(view); - effect.CurrentTechnique.Passes[0].Apply(); - device.Textures[0] = source; - device.Textures[1] = normals; - device.SamplerStates[0] = legacy ? SamplerState.PointWrap : SamplerState.LinearClamp; - device.SamplerStates[1] = SamplerState.PointClamp; - var vertices = new List(); - for (int i = 0; i <= 150; i++) - { - float angle = i * MathHelper.TwoPi / 150; - var direction = new Vector2(MathF.Cos(angle), MathF.Sin(angle)); - vertices.Add(new VertexPositionColorTexture(new Vector3(origin, -150), Color.White, new Vector2(0.5f))); - vertices.Add(new VertexPositionColorTexture(new Vector3(origin + direction * radius * scale, -10), - Color.White, new Vector2(0.5f) + direction * 0.27f)); - } - device.DrawUserPrimitives(PrimitiveType.TriangleStrip, vertices.ToArray(), 0, vertices.Count - 2); - device.SetRenderTarget(null); - var data = new Vector4[Size * Size]; - target.GetData(data); - return data; - } - - private static Vector4 Sample(Vector4[] data, int x, int y) => data[y * Size + x]; - - private static Vector2 Coordinates(Vector4 color) => new Vector2(color.X, color.Y) * Size; - - private void Check(bool condition, string message) - { - Console.WriteLine((condition ? "PASS " : "FAIL ") + message); - if (!condition) - { - Failures++; - } - } - } -} diff --git a/Tools/Tests/Test-KingJellyBallReflection.ps1 b/Tools/Tests/Test-KingJellyBallReflection.ps1 deleted file mode 100644 index e44966ee0..000000000 --- a/Tools/Tests/Test-KingJellyBallReflection.ps1 +++ /dev/null @@ -1,39 +0,0 @@ -# Run after dotnet build. Uses the installed tML FNA backend; does not load Terraria. -# -Legacy is a negative control: the previous shader should fail the shape tests. -param( - [Parameter(Mandatory = $true)] - [string]$TmlDirectory, - [string]$Configuration = 'Debug', - [switch]$Legacy -) - -$ErrorActionPreference = 'Stop' -$sourceRoot = (Resolve-Path (Join-Path $PSScriptRoot '../..')).Path -$fna = @(Get-ChildItem -LiteralPath (Join-Path $TmlDirectory 'Libraries') -Filter 'FNA.dll' -Recurse) -if ($fna.Count -ne 1) { throw 'Expected one FNA.dll in the tModLoader Libraries directory.' } -$probeDirectory = Join-Path ([IO.Path]::GetTempPath()) ('EverglowJellyReflection-' + [Guid]::NewGuid().ToString('N')) -New-Item -ItemType Directory -Path $probeDirectory | Out-Null -Copy-Item -LiteralPath (Join-Path $PSScriptRoot 'KingJellyBallReflectionProbe.cs') -Destination (Join-Path $probeDirectory 'Program.cs') -$fnaPath = [Security.SecurityElement]::Escape($fna[0].FullName) -$project = @" - - - Exe - net8.0 - enable - - $fnaPath - -"@ -[IO.File]::WriteAllText((Join-Path $probeDirectory 'Probe.csproj'), $project, [Text.UTF8Encoding]::new($false)) -$previousPath = $env:PATH -try { - $env:PATH = (Join-Path $TmlDirectory 'Libraries/Native/Windows') + [IO.Path]::PathSeparator + $previousPath - $probeArguments = @($sourceRoot, $Configuration) - if ($Legacy) { $probeArguments += '--legacy' } - dotnet run --project (Join-Path $probeDirectory 'Probe.csproj') -- @probeArguments - if ($LASTEXITCODE -ne 0) { throw "GPU reflection regression failed (exit $LASTEXITCODE)." } -} -finally { - $env:PATH = $previousPath -}