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");
|
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)
|
static void CheckWiring(GameSettings settings)
|
||||||
{
|
{
|
||||||
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single);
|
var scene = EditorSceneManager.OpenScene("Assets/Scenes/ARPG.unity", OpenSceneMode.Single);
|
||||||
|
|
@ -287,6 +315,8 @@ namespace Aetheria.EditorTools
|
||||||
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
EditorSceneManager.NewScene(NewSceneSetup.EmptyScene, NewSceneMode.Single);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CheckMenuWiring();
|
||||||
|
|
||||||
foreach (var path in new[]
|
foreach (var path in new[]
|
||||||
{
|
{
|
||||||
"Assets/Prefabs/RPG/Planets/Planet.prefab", "Assets/Prefabs/RPG/Planets/Gas Giant.prefab",
|
"Assets/Prefabs/RPG/Planets/Planet.prefab", "Assets/Prefabs/RPG/Planets/Gas Giant.prefab",
|
||||||
|
|
|
||||||
|
|
@ -10,13 +10,16 @@ using float2 = CultMath.float2;
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static class GravityField
|
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).
|
// 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.
|
// 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) =>
|
public static float Potential(float c, float coreRadius, float r) =>
|
||||||
c == 0f ? 0f :
|
c == 0f ? 0f :
|
||||||
r < coreRadius
|
r < coreRadius
|
||||||
? c * r * r / (2 * coreRadius * 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.
|
// The acceleration at `offsetFromBody`, toward the body: zero at the body itself.
|
||||||
public static float2 Acceleration(float c, float coreRadius, float2 offsetFromBody)
|
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
|
#ifndef AETHERIA_GRAVITY_INCLUDED
|
||||||
#define 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
|
// 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
|
// and the height is Zone.GetHeight; this file decides nothing. GravityLowering.Lower fills the buffer and the globals
|
||||||
// buffer and the globals from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame.
|
// from the zone (positions from Orbit.Position, reference from Zone.WellReference) each frame.
|
||||||
// GravityFieldParityVerify renders it and compares every texel with -Zone.GetHeight.
|
// GravityFieldParityVerify renders it and compares every texel with -Zone.GetHeight.
|
||||||
|
|
||||||
struct GravityWell
|
struct GravityWell
|
||||||
|
|
@ -18,13 +19,13 @@ StructuredBuffer<GravityWell> _GravityWells;
|
||||||
int _GravityWellCount;
|
int _GravityWellCount;
|
||||||
float _HeightExaggeration;
|
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)
|
float gravityPotential(float c, float R, float r)
|
||||||
{
|
{
|
||||||
return c == 0 ? 0 :
|
return c == 0 ? 0 :
|
||||||
r < R
|
r < R
|
||||||
? c * r * r / (2 * 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.
|
// 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