From ee18829a047bbee65e4ff2930e259b2c90609df6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 04:09:08 +0200 Subject: [PATCH] FlowFieldSceneVerify: scene-stardust-uncullable renders the dust 10000 below the camera Fails on the bounded Graphics.DrawProcedural (this commit builds; the check fails until the cut lands). Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Editor/FlowFieldSceneVerify.cs | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs index 80d9a4e31..bb459652c 100644 --- a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs +++ b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs @@ -140,6 +140,8 @@ namespace Aetheria.EditorTools Call(volume, "Update"); if (Differing(flowing, Frame()) == 0) Fail("scene-stardust-run", "the particle buffer did not move while the zone ran"); if (Shader.GetGlobalFloat("_NebulaNoiseTime") == noiseTime) Fail("scene-noise-clock", "_NebulaNoiseTime did not move while the zone ran"); + + CheckStardustUncullable(dust, bufferField); } finally { @@ -147,6 +149,77 @@ namespace Aetheria.EditorTools } } + // The dust is a command buffer on the target camera, which no culling sees: with every particle 10000 below the + // camera (far past any box a hand could inflate round it) a render still shows dust, and shows none when the + // same particles have no size. + static void CheckStardustUncullable(Stardust dust, FieldInfo bufferField) + { + var camera = dust.TargetCamera; + if (camera == null) { Fail("scene-stardust-uncullable", "Stardust has no TargetCamera"); return; } + var attached = camera.GetCommandBuffers(CameraEvent.AfterForwardOpaque).Count(buffer => buffer.name == "Stardust"); + if (attached != 1) Fail("scene-stardust-uncullable", $"{attached} Stardust command buffers on the target camera, expected 1"); + + var particles = (ComputeBuffer) bufferField.GetValue(dust); + var side = Mathf.RoundToInt(Mathf.Sqrt(particles.count)); + float[] Layout(float size) + { + var data = new float[particles.count * 7]; + for (var i = 0; i < particles.count; i++) + { + data[i * 7 + 0] = ((i % side) - side / 2f) * 4f; + data[i * 7 + 1] = -50000f; + data[i * 7 + 2] = ((i / side) - side / 2f) * 4f; + data[i * 7 + 3] = data[i * 7 + 4] = data[i * 7 + 5] = 1f; + data[i * 7 + 6] = size; + } + return data; + } + + var transform = camera.transform; + var (position, rotation) = (transform.position, transform.rotation); + var (far, flags, background, mask, target) = (camera.farClipPlane, camera.clearFlags, camera.backgroundColor, camera.cullingMask, camera.targetTexture); + var texture = RenderTexture.GetTemporary(64, 64, 24, RenderTextureFormat.ARGB32); + var readback = new Texture2D(64, 64, TextureFormat.RGBA32, false); + var previous = RenderTexture.active; + Color32[] Render(float size) + { + Call(dust, "Update"); // on a bounded draw this queues the draw for the camera; here it dispatches the compute + particles.SetData(Layout(size)); + camera.Render(); + RenderTexture.active = texture; + readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0); + readback.Apply(); + return readback.GetPixels32(); + } + try + { + transform.SetPositionAndRotation(new Vector3(0f, -40000f, 0f), Quaternion.Euler(90f, 0f, 0f)); + camera.farClipPlane = 30000f; + camera.clearFlags = CameraClearFlags.SolidColor; + camera.backgroundColor = Color.black; + camera.cullingMask = 0; + camera.targetTexture = texture; + var withDust = Render(5f); + var without = Render(0f); + var differing = 0; + for (var i = 0; i < withDust.Length; i++) + if (!withDust[i].Equals(without[i])) differing++; + if (differing == 0) Fail("scene-stardust-uncullable", "the dust 10000 below the camera drew no pixels"); + } + finally + { + RenderTexture.active = previous; + camera.targetTexture = target; + camera.cullingMask = mask; + camera.backgroundColor = background; + camera.clearFlags = flags; + camera.farClipPlane = far; + transform.SetPositionAndRotation(position, rotation); + RenderTexture.ReleaseTemporary(texture); + UnityEngine.Object.DestroyImmediate(readback); + } + } + // CloudShader's pass 0 compiles with the flow keywords (it must #include_with_pragmas the shared include). static void CheckCloudKeywords() {