Merge stardust-before-fog: Stardust draws before nebula composite (closes aetheria-gravity verdicts cut-stardust-before-fog.s1 and .s2)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
3eb9d4df6c
2 changed files with 145 additions and 17 deletions
|
|
@ -142,6 +142,7 @@ namespace Aetheria.EditorTools
|
|||
if (Shader.GetGlobalFloat("_NebulaNoiseTime") == noiseTime) Fail("scene-noise-clock", "_NebulaNoiseTime did not move while the zone ran");
|
||||
|
||||
CheckStardustUncullable(dust, bufferField);
|
||||
CheckStardustUnderFog(dust, bufferField);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
|
@ -149,6 +150,10 @@ namespace Aetheria.EditorTools
|
|||
}
|
||||
}
|
||||
|
||||
// The event Stardust itself attaches its draw at, read from the class so the verifier cannot agree with a copy of it.
|
||||
static CameraEvent DrawEvent() =>
|
||||
(CameraEvent) typeof(Stardust).GetField("DrawEvent", BindingFlags.NonPublic | BindingFlags.Static).GetRawConstantValue();
|
||||
|
||||
// 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.
|
||||
|
|
@ -156,24 +161,17 @@ namespace Aetheria.EditorTools
|
|||
{
|
||||
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 drawEvent = DrawEvent();
|
||||
var attached = camera.GetCommandBuffers(drawEvent).Count(buffer => buffer.name == "Stardust");
|
||||
if (attached != 1) Fail("scene-stardust-uncullable", $"{attached} Stardust command buffers at {drawEvent} on the target camera, expected 1");
|
||||
foreach (var other in Enum.GetValues(typeof(CameraEvent)).Cast<CameraEvent>().Where(other => other != drawEvent))
|
||||
{
|
||||
var stray = camera.GetCommandBuffers(other).Count(buffer => buffer.name == "Stardust");
|
||||
if (stray != 0) Fail("scene-stardust-uncullable", $"{stray} Stardust command buffers at {other}, expected 0");
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
float[] Layout(float size, Vector3 centre, float below) => DustLayout(particles.count, size, centre, below);
|
||||
|
||||
var transform = camera.transform;
|
||||
var (position, rotation) = (transform.position, transform.rotation);
|
||||
|
|
@ -231,6 +229,135 @@ namespace Aetheria.EditorTools
|
|||
}
|
||||
}
|
||||
|
||||
// The nebula is an [ImageEffectOpaque] that fogs the whole ray to a depth the dust never writes (B49): dust drawn
|
||||
// before it is attenuated by the fog, dust drawn after it sits on top at full strength. The dust's contribution to a
|
||||
// pixel (render with dust minus render without) is read with the cloud renderer off and on, in an HDR target.
|
||||
static void CheckStardustUnderFog(Stardust dust, FieldInfo bufferField)
|
||||
{
|
||||
const int Size = 128;
|
||||
var camera = dust.TargetCamera;
|
||||
var cloud = camera == null ? null : camera.GetComponent<VolumeCloudRenderer>();
|
||||
if (cloud == null) { Fail("scene-stardust-under-fog", "the target camera has no VolumeCloudRenderer"); return; }
|
||||
var particles = (ComputeBuffer) bufferField.GetValue(dust);
|
||||
|
||||
var transform = camera.transform;
|
||||
var (position, rotation) = (transform.position, transform.rotation);
|
||||
var (far, flags, background, mask, target, hdr) = (camera.farClipPlane, camera.clearFlags, camera.backgroundColor, camera.cullingMask, camera.targetTexture, camera.allowHDR);
|
||||
var texture = RenderTexture.GetTemporary(Size, Size, 24, RenderTextureFormat.ARGBHalf);
|
||||
var readback = new Texture2D(Size, Size, TextureFormat.RGBAHalf, false);
|
||||
var previous = RenderTexture.active;
|
||||
var effects = camera.GetComponents<MonoBehaviour>().Where(behaviour => behaviour.enabled).ToArray();
|
||||
var fields = new System.Collections.Generic.List<Texture2D>();
|
||||
GameObject floor = null;
|
||||
float[] Render(bool fog, float size)
|
||||
{
|
||||
cloud.enabled = fog;
|
||||
for (var frame = 0; frame < 2; frame++) // the cloud's first frame forces ULTRA and fills its history; the second is the settled one
|
||||
{
|
||||
Call(dust, "Update");
|
||||
particles.SetData(DustLayout(particles.count, size, position, 100f));
|
||||
camera.Render();
|
||||
}
|
||||
RenderTexture.active = texture;
|
||||
readback.ReadPixels(new Rect(0, 0, Size, Size), 0, 0);
|
||||
readback.Apply();
|
||||
return readback.GetPixels().Select(pixel => pixel.r * .2126f + pixel.g * .7152f + pixel.b * .0722f).ToArray();
|
||||
}
|
||||
try
|
||||
{
|
||||
foreach (var effect in effects) effect.enabled = false;
|
||||
camera.allowHDR = true;
|
||||
camera.clearFlags = CameraClearFlags.SolidColor;
|
||||
camera.backgroundColor = Color.black;
|
||||
camera.cullingMask = 1;
|
||||
camera.targetTexture = texture;
|
||||
camera.farClipPlane = 1000f;
|
||||
transform.SetPositionAndRotation(position, Quaternion.Euler(90f, 0f, 0f));
|
||||
// The fog only measures a partial density where depth is written: a pixel with no depth is fogged whole (CloudShader sets
|
||||
// density 1 past the far plane). A floor under the dust gives the march a finite ray.
|
||||
floor = GameObject.CreatePrimitive(PrimitiveType.Plane);
|
||||
floor.transform.position = position + Vector3.down * 200f;
|
||||
floor.transform.localScale = Vector3.one * 100f;
|
||||
UnityEngine.Object.DestroyImmediate(floor.GetComponent<Collider>());
|
||||
|
||||
// The scene's own nebula fields are textures the game renders; here a uniform fill of white fog stands in for them.
|
||||
foreach (var (name, format) in new[] { ("_NebulaSurfaceHeight", TextureFormat.RFloat), ("_NebulaPatchHeight", TextureFormat.RHalf), ("_NebulaPatch", TextureFormat.RHalf) })
|
||||
Shader.SetGlobalTexture(name, Uniform(format, Color.black, fields));
|
||||
Shader.SetGlobalTexture("_NebulaTint", Uniform(TextureFormat.RGBA32, Color.white, fields));
|
||||
Shader.DisableKeyword("NOISE_SLOPE");
|
||||
Shader.SetGlobalFloat("_NebulaFillDensity", .01f);
|
||||
Shader.SetGlobalFloat("_NebulaFillDistance", 120f);
|
||||
Shader.SetGlobalFloat("_NebulaFillExponent", 0f);
|
||||
Shader.SetGlobalFloat("_NebulaFillOffset", 0f);
|
||||
Shader.SetGlobalFloat("_NebulaPatchDensity", 0f);
|
||||
Shader.SetGlobalFloat("_NebulaLuminance", .01f); // a fog dimmer than the dust: dust drawn over it would otherwise read as a negative contribution
|
||||
Shader.SetGlobalFloat("_ExtinctionCoefficient", .25f); // with the fill density, about 40% of the light to the floor is fogged away
|
||||
Shader.SetGlobalFloat("_SafetyDistance", -1e6f); // below every sample: only the uniform fill, none of the zone's noise
|
||||
Shader.SetGlobalFloat("_TintLodExponent", 0f);
|
||||
|
||||
var clearBare = Render(false, 0f);
|
||||
var clearDust = Render(false, 5f);
|
||||
var fogBare = Render(true, 0f);
|
||||
var fogDust = Render(true, 5f);
|
||||
|
||||
var fogShown = 0f;
|
||||
var (clearSum, fogSum, drawn) = (0f, 0f, 0);
|
||||
for (var i = 0; i < clearBare.Length; i++)
|
||||
{
|
||||
fogShown += Mathf.Abs(fogBare[i] - clearBare[i]) / clearBare.Length;
|
||||
if (clearDust[i] == clearBare[i]) continue;
|
||||
drawn++; clearSum += clearDust[i] - clearBare[i]; fogSum += fogDust[i] - fogBare[i];
|
||||
}
|
||||
var (clearMean, fogMean) = (drawn == 0 ? 0f : clearSum / drawn, drawn == 0 ? 0f : fogSum / drawn);
|
||||
Debug.Log($"{Tag} stardust under fog: fog control mean |difference|={fogShown}, dust pixels={drawn}, dust contribution cloud off={clearMean}, cloud on={fogMean}, ratio={(clearMean == 0f ? 0f : fogMean / clearMean)}");
|
||||
if (drawn == 0 || clearMean <= 0f) Fail("scene-stardust-under-fog", "control: the dust drew no pixels with the cloud renderer off");
|
||||
else if (fogShown < 1e-3f) Fail("scene-stardust-under-fog", "control: the cloud renderer changed no pixels, so no fog is in view");
|
||||
else if (fogMean > .9f * clearMean) Fail("scene-stardust-under-fog", "the dust is not attenuated by the fog in front of it: it was drawn after the nebula composites");
|
||||
else if (fogMean < .1f * clearMean) Fail("scene-stardust-under-fog", "the dust is erased, not dimmed, by a partial fog");
|
||||
}
|
||||
finally
|
||||
{
|
||||
RenderTexture.active = previous;
|
||||
foreach (var effect in effects) effect.enabled = true;
|
||||
camera.allowHDR = hdr;
|
||||
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);
|
||||
foreach (var field in fields) UnityEngine.Object.DestroyImmediate(field);
|
||||
if (floor != null) UnityEngine.Object.DestroyImmediate(floor);
|
||||
}
|
||||
}
|
||||
|
||||
static Texture2D Uniform(TextureFormat format, Color colour, System.Collections.Generic.List<Texture2D> owner)
|
||||
{
|
||||
var texture = new Texture2D(1, 1, format, false) { wrapMode = TextureWrapMode.Clamp };
|
||||
texture.SetPixel(0, 0, colour);
|
||||
texture.Apply();
|
||||
owner.Add(texture);
|
||||
return texture;
|
||||
}
|
||||
|
||||
// A square patch of particles 4 apart, `below` under `centre`.
|
||||
static float[] DustLayout(int count, float size, Vector3 centre, float below)
|
||||
{
|
||||
var side = Mathf.RoundToInt(Mathf.Sqrt(count));
|
||||
var data = new float[count * 7];
|
||||
for (var i = 0; i < 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;
|
||||
}
|
||||
|
||||
// CloudShader's pass 0 compiles with the flow keywords (it must #include_with_pragmas the shared include).
|
||||
static void CheckCloudKeywords()
|
||||
{
|
||||
|
|
|
|||
Loading…
Reference in a new issue