From ee18829a047bbee65e4ff2930e259b2c90609df6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 04:09:08 +0200 Subject: [PATCH 1/5] 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() { From 03e91aede2aef53561b8dbbf7cc925ac58a90170 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 04:37:27 +0200 Subject: [PATCH 2/5] FlowFieldSceneVerify: stardust probe renders without the camera's image effects With them on, the effects wrote their own frame and the probe could not tell dust from noise. Builds; the check fails on the bounded draw (buffer count 0, no dust pixels, run against 5a788833 plus this probe). Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Editor/FlowFieldSceneVerify.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs index bb459652c..f49a1a19f 100644 --- a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs +++ b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.IO; using System.Linq; using System.Reflection; @@ -181,6 +181,8 @@ namespace Aetheria.EditorTools var texture = RenderTexture.GetTemporary(64, 64, 24, RenderTextureFormat.ARGB32); var readback = new Texture2D(64, 64, TextureFormat.RGBA32, false); var previous = RenderTexture.active; + // Image effects on the camera would write their own frame over the dust; only the dust draw is under test. + var effects = camera.GetComponents().Where(behaviour => behaviour.enabled).ToArray(); Color32[] Render(float size) { Call(dust, "Update"); // on a bounded draw this queues the draw for the camera; here it dispatches the compute @@ -193,6 +195,7 @@ namespace Aetheria.EditorTools } try { + foreach (var effect in effects) effect.enabled = false; transform.SetPositionAndRotation(new Vector3(0f, -40000f, 0f), Quaternion.Euler(90f, 0f, 0f)); camera.farClipPlane = 30000f; camera.clearFlags = CameraClearFlags.SolidColor; @@ -209,6 +212,7 @@ namespace Aetheria.EditorTools finally { RenderTexture.active = previous; + foreach (var effect in effects) effect.enabled = true; camera.targetTexture = target; camera.cullingMask = mask; camera.backgroundColor = background; From 71bf2a1bff60b5656ab20a9c1a4a6cb7520e3f6a Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 04:48:41 +0200 Subject: [PATCH 3/5] Stardust: draw by a command buffer on the target camera, never frustum culled Deletes the bounded Graphics.DrawProcedural, its hand-inflated Bounds, the GravityCamera field (and its scene reference) and the dead OnRenderObject block. The camera replays the buffer at AfterForwardOpaque; it is attached on enable, detached on disable and destroy, and released. Unity compile not yet run for this commit. Co-Authored-By: Claude Opus 5.5 --- Assets/Scenes/ARPG.unity | 1 - Assets/Shaders/Compute/Stardust/Stardust.cs | 61 ++++++++++++--------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/Assets/Scenes/ARPG.unity b/Assets/Scenes/ARPG.unity index 9c650e2f0..e80717ee0 100644 --- a/Assets/Scenes/ARPG.unity +++ b/Assets/Scenes/ARPG.unity @@ -16463,7 +16463,6 @@ MonoBehaviour: NebulaTint: {fileID: 8400000, guid: d196505464047f04a97f28689041209b, type: 2} Heightmap: {fileID: 8400000, guid: 2fc21a9d3e9647948b56e384cd9378b3, type: 2} ParticleColors: {fileID: 2800000, guid: 56db63da398b2d04394ddbccedce943c, type: 3} - GravityCamera: {fileID: 1537342003} --- !u!114 &1009503736 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Shaders/Compute/Stardust/Stardust.cs b/Assets/Shaders/Compute/Stardust/Stardust.cs index a6636fd2e..793154e3f 100644 --- a/Assets/Shaders/Compute/Stardust/Stardust.cs +++ b/Assets/Shaders/Compute/Stardust/Stardust.cs @@ -34,7 +34,6 @@ public class Stardust : MonoBehaviour public Texture Heightmap; public Texture2D ParticleColors; //public Transform TargetTransform; - public Camera GravityCamera; private const int GROUP_SIZE = 128; private int _updateParticlesKernel; @@ -57,6 +56,11 @@ public class Stardust : MonoBehaviour private ComputeBuffer _quadPoints; private const int QUAD_STRIDE = 12; + // The dust is drawn by a command buffer on TargetCamera: a camera replays its buffers at their event and never culls them, + // so the draw needs no bounds and no extent can fall short of how deep the field is. + private CommandBuffer _draw; + private const CameraEvent DrawEvent = CameraEvent.AfterForwardOpaque; + #endregion #region setup @@ -91,6 +95,33 @@ public class Stardust : MonoBehaviour new Vector3(-.5f, -.5f), new Vector3(-.5f, .5f) }); + + //The buffers never change after this, so the material is bound once + ParticleMaterial.SetBuffer("particles", _particlesBuffer); + ParticleMaterial.SetBuffer("quadPoints", _quadPoints); + + //Start may run again (the scene harness calls it by hand): drop the previous draw before building the next + DetachDraw(); + _draw?.Release(); + _draw = new CommandBuffer { name = "Stardust" }; + _draw.DrawProcedural(Matrix4x4.identity, ParticleMaterial, 0, MeshTopology.Triangles, 6, Span * Span); + AttachDraw(); + } + + void OnEnable() => AttachDraw(); + + void OnDisable() => DetachDraw(); + + void AttachDraw() + { + if (_draw != null && TargetCamera != null) + TargetCamera.AddCommandBuffer(DrawEvent, _draw); + } + + void DetachDraw() + { + if (_draw != null && TargetCamera != null) + TargetCamera.RemoveCommandBuffer(DrawEvent, _draw); } #endregion @@ -104,7 +135,6 @@ public class Stardust : MonoBehaviour // Particle lifetimes cycle on the flow clock (zone time), so the dust stops with the sim like everything the flow moves. ParticleCalculation.SetFloat("phase", FlowField.Cycle(VolumeSampling.FlowClock, Period)); - var pos = GravityCamera.transform.position; ParticleCalculation.SetFloat("period", Period); ParticleCalculation.SetFloat("spacing", Spacing); ParticleCalculation.SetFloat("ceilingHeight", Ceiling); @@ -121,38 +151,17 @@ public class Stardust : MonoBehaviour //Dispatch, launch threads on GPU int numberOfGroups = Mathf.CeilToInt((float)Span * Span / GROUP_SIZE); ParticleCalculation.Dispatch(_updateParticlesKernel, numberOfGroups, 1, 1); - - ParticleMaterial.SetBuffer("particles", _particlesBuffer); - ParticleMaterial.SetBuffer("quadPoints", _quadPoints); - Graphics.DrawProcedural(ParticleMaterial, new Bounds(pos, new Vector3(Spacing * Span, 2048, Spacing * Span)), MeshTopology.Triangles, 6, Span * Span, TargetCamera, null, ShadowCastingMode.Off, false, 0); } #endregion - #region rendering - // void OnRenderObject() - // { - // if (Camera.current == TargetCamera || Camera.current.name == "SceneCamera") - // { - // //Bind resources to material - // ParticleMaterial.SetBuffer("particles", _particlesBuffer); - // ParticleMaterial.SetBuffer("quadPoints", _quadPoints); - // // ParticleMaterial.SetFloat("velocityStretch", ParticleVelocityStretch); - // // ParticleMaterial.SetVector("velocity", PlayerVelocity); - // - // //Set the pass - // ParticleMaterial.SetPass(0); - // - // //Draw - // Graphics.DrawProceduralNow(MeshTopology.Triangles, 6, Span * Span); - // } - // } - #endregion - #region cleanup void OnDestroy() { // m_emitterProxies.Release(); // _temperature.Release(); + DetachDraw(); + _draw?.Release(); + _draw = null; if(_particlesBuffer!=null) { _particlesBuffer.Release(); From 8439e394345f81f3b1eef2ff38f482714920c7b6 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 05:24:33 +0200 Subject: [PATCH 4/5] FlowFieldSceneVerify: stardust probe sizes its particles to cover pixels at 10000 At size 5 the material's distance^.33 scaling made every quad sub-pixel, so the probe saw no dust with or without the cull. Builds. Co-Authored-By: Claude Opus 5.5 --- Assets/Scripts/Editor/FlowFieldSceneVerify.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs index f49a1a19f..ba1ca8ca3 100644 --- a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs +++ b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs @@ -202,7 +202,7 @@ namespace Aetheria.EditorTools camera.backgroundColor = Color.black; camera.cullingMask = 0; camera.targetTexture = texture; - var withDust = Render(5f); + var withDust = Render(400f); // the material scales a quad by distance^.33, so at this range it needs this size to cover pixels var without = Render(0f); var differing = 0; for (var i = 0; i < withDust.Length; i++) From 1973b6d22086f06c26a338fc15171f61e9a5f7d1 Mon Sep 17 00:00:00 2001 From: Meta Vens Date: Fri, 16 Oct 2026 05:46:50 +0200 Subject: [PATCH 5/5] Stardust: draw at BeforeForwardAlpha (deferred cameras never run AfterForwardOpaque); probe checks the event, culling mask 1 and a near control; Main Menu drops the stale GravityCamera Both target cameras are deferred, so the AfterForwardOpaque buffer never drew. The render half of scene-stardust-uncullable now attaches-checks BeforeForwardAlpha, renders with cullingMask 1 (DrawProcedural is layer 0), and requires a near control (dust 100 below, size 5) to draw before the far probe is read, so a miss at 10000 can only be culling. Builds: not compiled by this commit alone; verified in the Unity batchmode run reported with the cut. Co-Authored-By: Claude Opus 5.5 --- Assets/Scenes/Main Menu.unity | 1 - Assets/Scripts/Editor/FlowFieldSceneVerify.cs | 31 ++++++++++++------- Assets/Shaders/Compute/Stardust/Stardust.cs | 2 +- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/Assets/Scenes/Main Menu.unity b/Assets/Scenes/Main Menu.unity index c67fdb7cc..4fafe8336 100644 --- a/Assets/Scenes/Main Menu.unity +++ b/Assets/Scenes/Main Menu.unity @@ -892,7 +892,6 @@ MonoBehaviour: NebulaTint: {fileID: 8400000, guid: d196505464047f04a97f28689041209b, type: 2} Heightmap: {fileID: 8400000, guid: 2fc21a9d3e9647948b56e384cd9378b3, type: 2} ParticleColors: {fileID: 2800000, guid: 56db63da398b2d04394ddbccedce943c, type: 3} - GravityCamera: {fileID: 2103364771} --- !u!114 &750375536 MonoBehaviour: m_ObjectHideFlags: 0 diff --git a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs index ba1ca8ca3..771cd9396 100644 --- a/Assets/Scripts/Editor/FlowFieldSceneVerify.cs +++ b/Assets/Scripts/Editor/FlowFieldSceneVerify.cs @@ -156,19 +156,19 @@ namespace Aetheria.EditorTools { 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"); + var attached = camera.GetCommandBuffers(CameraEvent.BeforeForwardAlpha).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) + float[] Layout(float size, Vector3 centre, float below) { 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 + 0] = centre.x + ((i % side) - side / 2f) * 4f; + data[i * 7 + 1] = centre.y - below; + data[i * 7 + 2] = centre.z + ((i / side) - side / 2f) * 4f; data[i * 7 + 3] = data[i * 7 + 4] = data[i * 7 + 5] = 1f; data[i * 7 + 6] = size; } @@ -183,10 +183,10 @@ namespace Aetheria.EditorTools var previous = RenderTexture.active; // Image effects on the camera would write their own frame over the dust; only the dust draw is under test. var effects = camera.GetComponents().Where(behaviour => behaviour.enabled).ToArray(); - Color32[] Render(float size) + Color32[] Render(float size, Vector3 centre, float below) { Call(dust, "Update"); // on a bounded draw this queues the draw for the camera; here it dispatches the compute - particles.SetData(Layout(size)); + particles.SetData(Layout(size, centre, below)); camera.Render(); RenderTexture.active = texture; readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0); @@ -196,14 +196,21 @@ namespace Aetheria.EditorTools try { foreach (var effect in effects) effect.enabled = false; - 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.cullingMask = 1; camera.targetTexture = texture; - var withDust = Render(400f); // the material scales a quad by distance^.33, so at this range it needs this size to cover pixels - var without = Render(0f); + // control: the camera where the scene put it, dust 100 below, inside any box a hand-set extent would give + transform.SetPositionAndRotation(position, Quaternion.Euler(90f, 0f, 0f)); + camera.farClipPlane = 1000f; + var nearWith = Render(5f, position, 100f); var nearWithout = Render(0f, position, 100f); + var nearDiff = 0; for (var i = 0; i < nearWith.Length; i++) if (!nearWith[i].Equals(nearWithout[i])) nearDiff++; + Debug.Log($"{Tag} stardust near-control differing pixels={nearDiff}"); + if (nearDiff == 0) Fail("scene-stardust-uncullable", "control: dust 100 below the camera drew no pixels"); + transform.SetPositionAndRotation(new Vector3(0f, -40000f, 0f), Quaternion.Euler(90f, 0f, 0f)); + camera.farClipPlane = 30000f; + var withDust = Render(400f, transform.position, 10000f); // the material scales a quad by distance^.33, so at this range it needs this size to cover pixels + var without = Render(0f, transform.position, 10000f); var differing = 0; for (var i = 0; i < withDust.Length; i++) if (!withDust[i].Equals(without[i])) differing++; diff --git a/Assets/Shaders/Compute/Stardust/Stardust.cs b/Assets/Shaders/Compute/Stardust/Stardust.cs index 793154e3f..8b8b45c22 100644 --- a/Assets/Shaders/Compute/Stardust/Stardust.cs +++ b/Assets/Shaders/Compute/Stardust/Stardust.cs @@ -59,7 +59,7 @@ public class Stardust : MonoBehaviour // The dust is drawn by a command buffer on TargetCamera: a camera replays its buffers at their event and never culls them, // so the draw needs no bounds and no extent can fall short of how deep the field is. private CommandBuffer _draw; - private const CameraEvent DrawEvent = CameraEvent.AfterForwardOpaque; + private const CameraEvent DrawEvent = CameraEvent.BeforeForwardAlpha; #endregion