Gravity.cginc is generated: GravityShader.Source() is its one author, a test pins the committed file to it byte for byte
The mirror test compared a normalised reading of two spellings and was beaten twice (a statement before the return, gravityHeight's sum). GravityShader writes the whole file (potential, zero-strength guard, reference term, height sum) from GravityField's constant (TailOffset); GravityShaderTests fails on any difference. GravityShaderMirrorTests deleted. GravityFieldParityVerify opens Main Menu.unity: no ZoneRenderer there, its Fog Surface Displacement camera is the menu's own brush presentation (layers 9 and 10) by spec. Builds not yet verified on this commit; verified at the final merge. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
e6d1546b44
commit
13c7a08707
7 changed files with 138 additions and 55 deletions
|
|
@ -251,6 +251,34 @@ namespace Aetheria.EditorTools
|
|||
if (compared < Size * Size / 2) Fail("gradient", $"only {compared} texels were compared");
|
||||
}
|
||||
|
||||
// Main Menu.unity has no ZoneRenderer, so no Gravity Field and no lowered potential: its gravity cameras draw the
|
||||
// menu's own brush objects (Boundary on layer 9, the Waves on layer 10) and are presentation by spec (follow-up
|
||||
// menu-grid-brushes), not a reader of the potential. This pins that: a ZoneRenderer in the menu would make its
|
||||
// cameras readers of the field, and they would have to cull layer 9 alone like ARPG's.
|
||||
static void CheckMenuWiring()
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/Main Menu.unity", OpenSceneMode.Single);
|
||||
try
|
||||
{
|
||||
if (UnityEngine.Object.FindFirstObjectByType<ZoneRenderer>(FindObjectsInactive.Include) != null)
|
||||
Fail("wiring", "Main Menu.unity has a ZoneRenderer: its gravity cameras now read the Gravity Field and must cull layer 9 alone");
|
||||
var seen = 0;
|
||||
foreach (var camera in UnityEngine.Object.FindObjectsByType<Camera>(FindObjectsInactive.Include, FindObjectsSortMode.None))
|
||||
{
|
||||
if (camera.targetTexture == null || camera.targetTexture.name != "Fog Surface Displacement") continue;
|
||||
seen++;
|
||||
const int menuBrushLayers = (1 << 9) | (1 << 10);
|
||||
if (camera.cullingMask != menuBrushLayers)
|
||||
Fail("wiring", $"Main Menu camera {camera.name} culls mask {camera.cullingMask}, not the menu brush layers 9 and 10");
|
||||
}
|
||||
if (seen != 1) Fail("wiring", $"{seen} Fog Surface Displacement cameras in Main Menu.unity, expected 1");
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckWiring(GameSettings settings)
|
||||
{
|
||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single);
|
||||
|
|
@ -287,6 +315,8 @@ namespace Aetheria.EditorTools
|
|||
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||
}
|
||||
|
||||
CheckMenuWiring();
|
||||
|
||||
foreach (var path in new[]
|
||||
{
|
||||
"Assets/Prefabs/RPG/Planets/Planet.prefab", "Assets/Prefabs/RPG/Planets/Gas Giant.prefab",
|
||||
|
|
|
|||
|
|
@ -10,13 +10,16 @@ using float2 = CultMath.float2;
|
|||
/// </summary>
|
||||
public static class GravityField
|
||||
{
|
||||
// The tail's additive constant: it makes the tail meet the core in value at the core radius. Gravity.cginc is generated from it.
|
||||
public const float TailOffset = .5f;
|
||||
|
||||
// The potential at distance r from the body, defined up to a constant (callers subtract their reference).
|
||||
// A well of no strength (a body whose curves are unauthored, so c = 0 and R = 0) digs nothing.
|
||||
public static float Potential(float c, float coreRadius, float r) =>
|
||||
c == 0f ? 0f :
|
||||
r < coreRadius
|
||||
? c * r * r / (2 * coreRadius * coreRadius)
|
||||
: c * (log(r / coreRadius) + .5f);
|
||||
: c * (log(r / coreRadius) + TailOffset);
|
||||
|
||||
// The acceleration at `offsetFromBody`, toward the body: zero at the body itself.
|
||||
public static float2 Acceleration(float c, float coreRadius, float2 offsetFromBody)
|
||||
|
|
|
|||
65
Assets/Scripts/ServerShared/GravityShader.cs
Normal file
65
Assets/Scripts/ServerShared/GravityShader.cs
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
using System.Globalization;
|
||||
|
||||
/// <summary>
|
||||
/// The single author of Assets/Shaders/Gravity.cginc, the GPU lowering of the zone's potential. The law is
|
||||
/// GravityField's (its constants are read here, not respelled) and the height is Zone.GetHeight: each well's
|
||||
/// potential minus its reference, summed, divided by the exaggeration. Unity does not run C# when it imports a
|
||||
/// shader, so the committed cginc is this output; GravityShaderTests fails unless it equals Source() exactly.
|
||||
/// To change the shader, change this file and regenerate: AETHERIA_REGENERATE_GRAVITY_CGINC=1 dotnet test --filter GravityShaderTests.
|
||||
/// </summary>
|
||||
public static class GravityShader
|
||||
{
|
||||
public const string Path = "Assets/Shaders/Gravity.cginc";
|
||||
|
||||
/// <summary>The whole file, with LF line ends.</summary>
|
||||
public static string Source() => (Template
|
||||
.Replace("@TAIL@", GravityField.TailOffset.ToString("R", CultureInfo.InvariantCulture)))
|
||||
.Replace("\r\n", "\n");
|
||||
|
||||
private const string Template = """
|
||||
#ifndef AETHERIA_GRAVITY_INCLUDED
|
||||
#define AETHERIA_GRAVITY_INCLUDED
|
||||
|
||||
// GENERATED by ServerShared/GravityShader.cs from GravityField; do not edit (GravityShaderTests fails on any difference).
|
||||
// The lowering of the zone's potential (aetheria-gravity, cut grid-lowering). The law is ServerShared's GravityField
|
||||
// and the height is Zone.GetHeight; this file decides nothing. GravityLowering.Lower fills the buffer and the globals
|
||||
// from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame.
|
||||
// GravityFieldParityVerify renders it and compares every texel with -Zone.GetHeight.
|
||||
|
||||
struct GravityWell
|
||||
{
|
||||
float2 position;
|
||||
float strength;
|
||||
float coreRadius;
|
||||
float reference;
|
||||
};
|
||||
|
||||
StructuredBuffer<GravityWell> _GravityWells;
|
||||
int _GravityWellCount;
|
||||
float _HeightExaggeration;
|
||||
|
||||
// GravityField.Potential: c is the well's strength, R its core radius, r the distance.
|
||||
float gravityPotential(float c, float R, float r)
|
||||
{
|
||||
return c == 0 ? 0 :
|
||||
r < R
|
||||
? c * r * r / (2 * R * R)
|
||||
: c * (log(r / R) + @TAIL@);
|
||||
}
|
||||
|
||||
// Zone.GetHeight at world xz: each well's potential minus its own reference, summed, divided by the exaggeration.
|
||||
float gravityHeight(float2 p)
|
||||
{
|
||||
float result = 0;
|
||||
for (int i = 0; i < _GravityWellCount; i++)
|
||||
{
|
||||
GravityWell well = _GravityWells[i];
|
||||
result += gravityPotential(well.strength, well.coreRadius, length(p - well.position)) - well.reference;
|
||||
}
|
||||
return result / _HeightExaggeration;
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
""";
|
||||
}
|
||||
11
Assets/Scripts/ServerShared/GravityShader.cs.meta
Normal file
11
Assets/Scripts/ServerShared/GravityShader.cs.meta
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
fileFormatVersion: 2
|
||||
guid: 9a3fa1e9df764459b709270e58e79974
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
|
|
@ -1,9 +1,10 @@
|
|||
#ifndef AETHERIA_GRAVITY_INCLUDED
|
||||
#define AETHERIA_GRAVITY_INCLUDED
|
||||
|
||||
// GENERATED by ServerShared/GravityShader.cs from GravityField; do not edit (GravityShaderTests fails on any difference).
|
||||
// The lowering of the zone's potential (aetheria-gravity, cut grid-lowering). The law is ServerShared's GravityField
|
||||
// and the height is Zone.GetHeight; this file mirrors them and decides nothing. GravityLowering.Lower fills the
|
||||
// buffer and the globals from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame.
|
||||
// and the height is Zone.GetHeight; this file decides nothing. GravityLowering.Lower fills the buffer and the globals
|
||||
// from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame.
|
||||
// GravityFieldParityVerify renders it and compares every texel with -Zone.GetHeight.
|
||||
|
||||
struct GravityWell
|
||||
|
|
@ -18,13 +19,13 @@ StructuredBuffer<GravityWell> _GravityWells;
|
|||
int _GravityWellCount;
|
||||
float _HeightExaggeration;
|
||||
|
||||
// GravityField.Potential, line for line: c is the well's strength, R its core radius, r the distance.
|
||||
// GravityField.Potential: c is the well's strength, R its core radius, r the distance.
|
||||
float gravityPotential(float c, float R, float r)
|
||||
{
|
||||
return c == 0 ? 0 :
|
||||
r < R
|
||||
? c * r * r / (2 * R * R)
|
||||
: c * (log(r / R) + .5);
|
||||
: c * (log(r / R) + 0.5);
|
||||
}
|
||||
|
||||
// Zone.GetHeight at world xz: each well's potential minus its own reference, summed, divided by the exaggeration.
|
||||
|
|
|
|||
|
|
@ -1,50 +0,0 @@
|
|||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using Xunit;
|
||||
|
||||
// potential-owns-gravity (aetheria-gravity, cut grid-lowering): Assets/Shaders/Gravity.cginc lowers the potential for
|
||||
// the GPU and decides nothing, so its gravityPotential must be GravityField.Potential, term for term, zero-strength
|
||||
// guard and core branch included. The shader cannot be compiled here (GravityFieldParityVerify renders it, on Starfire
|
||||
// only), so this test is the automated guard: each copy's expression is read from its file, reduced to the same
|
||||
// spelling (float suffixes, the core radius's name, whitespace and comments), and compared. A drift in either file fails it.
|
||||
public sealed class GravityShaderMirrorTests
|
||||
{
|
||||
private static string Read(string relative) =>
|
||||
File.ReadAllText(Path.Combine(RunStartTests.FindRepoRoot(), relative));
|
||||
|
||||
// The text between `start` and the first `;` after it, comments removed and spellings reduced.
|
||||
private static string Expression(string source, string start, string then = "")
|
||||
{
|
||||
source = Regex.Replace(source, @"//[^\n]*", "");
|
||||
var from = source.IndexOf(start, System.StringComparison.Ordinal);
|
||||
Assert.True(from >= 0, $"'{start}' not found");
|
||||
var body = source.Substring(from + start.Length);
|
||||
if (then != "")
|
||||
{
|
||||
var at = body.IndexOf(then, System.StringComparison.Ordinal);
|
||||
Assert.True(at >= 0, $"'{then}' not found after '{start}'");
|
||||
body = body.Substring(at + then.Length);
|
||||
}
|
||||
body = body.Substring(0, body.IndexOf(';'));
|
||||
body = Regex.Replace(body, @"(?<=[\d.])f\b", "");
|
||||
body = body.Replace("coreRadius", "R");
|
||||
return Regex.Replace(body, @"\s+", "");
|
||||
}
|
||||
|
||||
private static string CSharpLaw() =>
|
||||
Expression(Read("Assets/Scripts/ServerShared/GravityField.cs"), "Potential(float c, float coreRadius, float r) =>");
|
||||
|
||||
private static string ShaderLaw() =>
|
||||
Expression(Read("Assets/Shaders/Gravity.cginc"), "float gravityPotential(float c, float R, float r)", "return");
|
||||
|
||||
[Fact]
|
||||
public void TheShaderRunsTheSameLawAsGravityFieldPotential()
|
||||
{
|
||||
var law = CSharpLaw();
|
||||
// The reduction found the whole law, so equality is not two empty strings agreeing.
|
||||
Assert.Contains("log(r/R)", law);
|
||||
Assert.Contains("c==0?0:", law);
|
||||
Assert.Contains("c*r*r/(2*R*R)", law);
|
||||
Assert.Equal(law, ShaderLaw());
|
||||
}
|
||||
}
|
||||
23
tests/Aetheria.Shared.Tests/GravityShaderTests.cs
Normal file
23
tests/Aetheria.Shared.Tests/GravityShaderTests.cs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
using System;
|
||||
using System.IO;
|
||||
using Xunit;
|
||||
|
||||
// potential-owns-gravity (aetheria-gravity, cut grid-lowering): Assets/Shaders/Gravity.cginc is the output of
|
||||
// GravityShader.Source(), which reads GravityField's constants and writes the one lowering of the potential, the
|
||||
// zero-strength guard, each well's reference and gravityHeight's sum. The committed file must equal it, character for
|
||||
// character (line ends aside): any added statement, respelling, flipped sign, swapped argument or macro fails here.
|
||||
// To change the shader, change GravityShader and run once with AETHERIA_REGENERATE_GRAVITY_CGINC=1.
|
||||
public sealed class GravityShaderTests
|
||||
{
|
||||
[Fact]
|
||||
public void TheCommittedShaderIsTheGeneratedOne()
|
||||
{
|
||||
var path = Path.Combine(RunStartTests.FindRepoRoot(), GravityShader.Path);
|
||||
if (Environment.GetEnvironmentVariable("AETHERIA_REGENERATE_GRAVITY_CGINC") == "1")
|
||||
File.WriteAllText(path, GravityShader.Source());
|
||||
var generated = GravityShader.Source();
|
||||
// The law was generated, not an empty template agreeing with an empty file.
|
||||
Assert.Contains("log(r / R) + 0.5", generated);
|
||||
Assert.Equal(generated, File.ReadAllText(path).Replace("\r\n", "\n"));
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue