Merge eureka/aetheria-gravity-stardust-bounds (closing verdicts cut-stardust-bounds.s1, cut-stardust-bounds.s2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-16 06:25:33 +02:00
commit fdecde02d0
4 changed files with 120 additions and 29 deletions

View file

@ -16461,7 +16461,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

View file

@ -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

View file

@ -1,4 +1,4 @@
using System;
using System;
using System.IO;
using System.Linq;
using System.Reflection;
@ -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,88 @@ 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.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, Vector3 centre, float below)
{
var data = new float[particles.count * 7];
for (var i = 0; i < particles.count; i++)
{
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;
}
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;
// Image effects on the camera would write their own frame over the dust; only the dust draw is under test.
var effects = camera.GetComponents<MonoBehaviour>().Where(behaviour => behaviour.enabled).ToArray();
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, centre, below));
camera.Render();
RenderTexture.active = texture;
readback.ReadPixels(new Rect(0, 0, 64, 64), 0, 0);
readback.Apply();
return readback.GetPixels32();
}
try
{
foreach (var effect in effects) effect.enabled = false;
camera.clearFlags = CameraClearFlags.SolidColor;
camera.backgroundColor = Color.black;
camera.cullingMask = 1;
camera.targetTexture = texture;
// 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++;
if (differing == 0) Fail("scene-stardust-uncullable", "the dust 10000 below the camera drew no pixels");
}
finally
{
RenderTexture.active = previous;
foreach (var effect in effects) effect.enabled = true;
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()
{

View file

@ -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.BeforeForwardAlpha;
#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();