Flow: register the flow keywords, one zone-time value for Zone.Flow and the scroll, presenter phases from zone time, folded scroll, tighter parity

- VolumeSampling registers FLOW_GLOBAL / FLOW_SLOPE with GlobalKeyword.Create (the constructor only looks up).
- FlowField.Scroll folds each Tri3D octave's phase modulo 1 in double (float2, [0,1)); Zone.Flow evaluates at Zone.FlowScroll.
- FlowField.Cycle: the nebula's flow cycle and Stardust's particle lifetime phase come from zone time (_FlowCycle, phase uniform), not _Time.y / Time.time.
- FlowFieldParityVerify reaches the keyword-gated flow(), fails on logged errors, tolerance 3e-4; mutants updated.

Not built or run when committed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Meta Vens 2026-10-13 21:43:41 +02:00
commit 0a3180d386
10 changed files with 178 additions and 55 deletions

View file

@ -21,10 +21,21 @@ namespace Aetheria.EditorTools
public static class FlowFieldParityVerify
{
const string Tag = "[FlowFieldParityVerify]";
const float RelativeTolerance = 1e-3f;
// Measured disagreement between the HLSL and the C# on this tree (float32 on both sides, positions to 10000,
// scrolls to 100): see the "max error" line the run prints; the last measurement is recorded in the report that
// landed this value. The tolerance is a small multiple of that noise so a 0.05% constant error (y factor .501,
// swirl or amplitude x1.0005) fails.
const float RelativeTolerance = 3e-4f;
static float _maxError;
static bool _ok = true;
static void OnLog(string message, string stack, LogType type)
{
if ((type == LogType.Error || type == LogType.Exception || type == LogType.Assert) && !message.StartsWith(Tag))
Fail("logged-error", message.Split('
')[0]);
}
static void Fail(string check, string detail)
{
Debug.LogError($"{Tag} FAIL {check}: {detail}");
@ -40,6 +51,8 @@ namespace Aetheria.EditorTools
return;
}
// The flow's error lines (a shader keyword that does not exist, a missing property) fail the check.
Application.logMessageReceived += OnLog;
var work = Path.Combine(Path.GetTempPath(), "flow-parity-" + Guid.NewGuid().ToString("N"));
Directory.CreateDirectory(work);
try
@ -72,6 +85,7 @@ namespace Aetheria.EditorTools
}
finally
{
Application.logMessageReceived -= OnLog;
try { Directory.Delete(work, true); } catch { }
}
EditorApplication.Exit(_ok ? 0 : 1);
@ -123,6 +137,7 @@ namespace Aetheria.EditorTools
var gradientBuffer = new ComputeBuffer(count, 8);
var globalBuffer = new ComputeBuffer(count, 12);
var slopeBuffer = new ComputeBuffer(count, 12);
var gatedBuffer = new ComputeBuffer(count, 12);
try
{
positionBuffer.SetData(positions);
@ -131,6 +146,7 @@ namespace Aetheria.EditorTools
compute.SetBuffer(kernel, "gradients", gradientBuffer);
compute.SetBuffer(kernel, "globalOut", globalBuffer);
compute.SetBuffer(kernel, "slopeOut", slopeBuffer);
compute.SetBuffer(kernel, "gatedOut", gatedBuffer);
compute.SetInt("count", count);
var elapsed = 0.0;
@ -143,16 +159,15 @@ namespace Aetheria.EditorTools
zone.Update(dt);
elapsed += dt;
}
var expectedScroll = flow.GlobalScrollSpeed * elapsed;
var scrollTolerance = 1e-4 * Math.Max(1.0, expectedScroll);
if (Math.Abs(zone.FlowScroll - expectedScroll) > scrollTolerance)
Fail($"{label}-zone-scroll", $"at zone time {elapsed:F3} FlowScroll {zone.FlowScroll} but speed x time is {expectedScroll}");
VolumeSampling.LowerFlow(zone.Settings.Flow, zone.ExactTime);
VolumeSampling.LowerFlow(zone.Settings.Flow, zone.FlowScroll);
var lowered = Shader.GetGlobalFloat("_FlowScroll");
if (Math.Abs(lowered - expectedScroll) > scrollTolerance)
Fail($"{label}-lowered-scroll", $"at zone time {elapsed:F3} the shader got _FlowScroll {lowered}, speed x time is {expectedScroll}");
// The shader got the zone's scroll and cycle: the same zone time Zone.Flow evaluates at.
var zoneScroll = zone.FlowScroll;
var lowered = Shader.GetGlobalVector("_FlowScroll");
if (Math.Abs(lowered.x - zoneScroll.x) > 1e-6 || Math.Abs(lowered.y - zoneScroll.y) > 1e-6)
Fail($"{label}-lowered-scroll", $"at zone time {elapsed:F3} the shader got _FlowScroll ({lowered.x}, {lowered.y}), the zone's is ({zoneScroll.x}, {zoneScroll.y})");
if (Math.Abs(Shader.GetGlobalFloat("_FlowCycle") - zone.CyclePhase(flow.Period)) > 1e-6)
Fail($"{label}-lowered-cycle", $"at zone time {elapsed:F3} the shader got _FlowCycle {Shader.GetGlobalFloat("_FlowCycle")}, the zone's phase is {zone.CyclePhase(flow.Period)}");
if (Shader.GetGlobalFloat("_FlowScale") != flow.GlobalScale || Shader.GetGlobalFloat("_FlowAmplitude") != flow.GlobalAmplitude ||
Shader.GetGlobalFloat("_FlowSlopeAmplitude") != flow.SlopeAmplitude ||
Shader.GetGlobalFloat("_FlowSwirlAmplitude") != flow.SwirlAmplitude || Shader.GetGlobalFloat("_FlowPeriod") != flow.Period)
@ -161,17 +176,31 @@ namespace Aetheria.EditorTools
compute.Dispatch(kernel, (count + 63) / 64, 1, 1);
var globalOut = new Vector3[count];
var slopeOut = new Vector3[count];
var gatedOut = new Vector3[count];
globalBuffer.GetData(globalOut);
slopeBuffer.GetData(slopeOut);
gatedBuffer.GetData(gatedOut);
var zoneScroll = zone.FlowScroll;
var at = $"{label} zone time {elapsed:F2} scroll {zoneScroll:F4}";
var at = $"{label} zone time {elapsed:F2} scroll {zoneScroll.x:F4}";
for (var i = 0; i < positionCount; i++)
{
var p = positions[i];
var expected = FlowField.Global(flow, new CultMath.float3(p.x, p.y, p.z), zoneScroll);
Compare($"{label}-global", at, i, new Vector3(expected.x, expected.y, expected.z), globalOut[i], flow.GlobalAmplitude);
}
// flow() is the keyword-gated function the nebula and stardust call; the zone's flat well has a zero
// gradient, so Zone.Flow is the global term here. A flow that is live must not read zero.
var live = 0;
for (var i = 0; i < positionCount; i++)
{
var p = positions[i];
var expected = zone.Flow(new CultMath.float3(p.x, p.y, p.z));
var e = new Vector3(expected.x, expected.y, expected.z);
if (e.magnitude > .1f * flow.GlobalAmplitude) live++;
Compare($"{label}-flow-keywords", at, i, e, gatedOut[i], flow.GlobalAmplitude);
}
if (flow.GlobalAmplitude != 0f && live < positionCount / 4)
Fail($"{label}-flow-keywords", $"{at}: only {live} of {positionCount} zone flows are live; the check is vacuous");
var slopeScale = Math.Max(Math.Abs(flow.SlopeAmplitude), Math.Abs(flow.SwirlAmplitude));
for (var i = 0; i < gradientCount; i++)
{
@ -183,7 +212,7 @@ namespace Aetheria.EditorTools
}
finally
{
positionBuffer.Release(); gradientBuffer.Release(); globalBuffer.Release(); slopeBuffer.Release();
positionBuffer.Release(); gradientBuffer.Release(); globalBuffer.Release(); slopeBuffer.Release(); gatedBuffer.Release();
}
}

View file

@ -11,15 +11,30 @@ using float3 = CultMath.float3;
/// </summary>
public static class FlowField
{
// The scroll offset of the noise field, in field units: speed times zone time and nothing else.
public static float Scroll(FlowSettings settings, double zoneTime) => (float) (settings.GlobalScrollSpeed * zoneTime);
// The second octave of the noise samples at position * Golden (Volumetric.cginc Tri3D).
const float Golden = 1.61803398875f;
// The scroll of the noise field, in field units: speed times zone time, for each of the two octaves, as a phase.
// Every term of Tri3D reaches the scrolled axis through frac, so a phase is only meaningful modulo 1: the fold
// happens here in double, where zone time is exact, and the float that reaches the shaders is in [0, 1) however
// old the zone is. x is the first octave's phase, y the second's (the second octave's axis is scaled by Golden).
public static float2 Scroll(FlowSettings settings, double zoneTime)
{
var scroll = (double) settings.GlobalScrollSpeed * zoneTime;
return float2((float) frac(scroll), (float) frac(scroll * Golden));
}
// The phase of a cycle of `period` seconds at zone time, in [0, 1). The nebula's flow-advected noise cycles and
// Stardust's particle lifetimes take their phase from here, so drift follows sim time and stops with the sim.
public static float Cycle(double zoneTime, float period) => period == 0f ? 0f : (float) frac(zoneTime / period);
// The procedural term (Volumetric.cginc globalFlow): a Tri3D field of position / scale, scrolled along y.
// Skipped when its amplitude is zero, as the shader's FLOW_GLOBAL keyword skips it.
public static float3 Global(FlowSettings settings, float3 position, float scroll)
public static float3 Global(FlowSettings settings, float3 position, float2 scroll)
{
if (settings.GlobalAmplitude == 0f) return float3.zero;
var sample = Tri3D(position / settings.GlobalScale - float3(0f, scroll, 0f)) * settings.GlobalAmplitude;
var field = position / settings.GlobalScale;
var sample = Tri3D(field - float3(0f, scroll.x, 0f), field * Golden - float3(0f, scroll.y, 0f)) * settings.GlobalAmplitude;
sample.y *= .5f;
return sample;
}
@ -37,8 +52,8 @@ public static class FlowField
+ float3(direction.x, 0f, direction.y) * settings.SlopeAmplitude;
}
public static float3 At(FlowSettings settings, float3 position, double zoneTime, float2 downhill) =>
Global(settings, position, Scroll(settings, zoneTime)) + Slope(settings, downhill);
public static float3 At(FlowSettings settings, float3 position, float2 scroll, float2 downhill) =>
Global(settings, position, scroll) + Slope(settings, downhill);
// Volumetric.cginc tri / tri3 / Tri3D, term for term.
static float Tri(float x) => abs(frac(x) - .5f);
@ -46,10 +61,11 @@ public static class FlowField
static float3 Tri3(float3 p) =>
float3(Tri(p.z + Tri(p.y)), Tri(p.z + Tri(p.x)), Tri(p.y + Tri(p.x)));
static float3 Tri3D(float3 p)
// The two octaves' positions are given already scrolled (see Scroll).
static float3 Tri3D(float3 p, float3 pGolden)
{
var t1 = normalize(Tri3(p));
var t2 = normalize(Tri3(p * 1.61803398875f));
var t2 = normalize(Tri3(pGolden));
return cross(t1, t2);
}
}

View file

@ -110,7 +110,7 @@ public class Zone
}
// Zone time at full precision, for the index's orbital keys, which must agree with ChunkPose's own.
internal double ExactTime => _time;
public double ExactTime => _time;
public ZonePack Pack { get; }
public GalaxyZone GalaxyZone { get; }
public Galaxy Galaxy { get; }
@ -696,11 +696,14 @@ public class Zone
public float3 Flow(float3 position)
{
var normal = GetNormal(position.xz);
return FlowField.At(Settings.Flow, position, _time, new float2(normal.x, normal.z));
return FlowField.At(Settings.Flow, position, FlowScroll, new float2(normal.x, normal.z));
}
// The flow's noise scroll at zone time; lowered to the shaders' _FlowScroll.
public float FlowScroll => FlowField.Scroll(Settings.Flow, _time);
// The flow's noise scroll at zone time (Zone.Flow evaluates at exactly this); lowered to the shaders' _FlowScroll.
public float2 FlowScroll => FlowField.Scroll(Settings.Flow, _time);
// The phase of a cycle of `period` seconds at zone time, for presenters that animate on the flow.
public float CyclePhase(float period) => FlowField.Cycle(_time, period);
public static float PowerPulse(float x, float exponent)
{

View file

@ -89,27 +89,34 @@ public class VolumeSampling : MonoBehaviour
Shader.SetGlobalFloat("_NebulaNoiseSlopeExponent", environment.Noise.SlopeExponent);
// The flow is the zone's: its settings and its time. Without a zone (menus) the same law runs on the presentation clock.
var zone = ActionGameManager.Instance?.Zone;
if (zone != null) LowerFlow(zone.Settings.Flow, zone.FlowScroll);
else LowerFlow(Settings.PlanetSettings.Flow, FlowField.Scroll(Settings.PlanetSettings.Flow, Time.timeSinceLevelLoad));
LowerFlow(ActionGameManager.Instance?.Zone?.Settings.Flow ?? Settings.PlanetSettings.Flow, FlowClock);
Shader.SetKeyword(_keywordNoiseSlope, environment.Noise.SlopeExponent != 0);
}
static readonly GlobalKeyword KeywordFlowGlobal = new GlobalKeyword("FLOW_GLOBAL");
static readonly GlobalKeyword KeywordFlowSlope = new GlobalKeyword("FLOW_SLOPE");
// GlobalKeyword's constructor only looks a keyword up; Create registers it. The shaders' pragmas and this owner agree on the names.
static readonly GlobalKeyword KeywordFlowGlobal = GlobalKeyword.Create("FLOW_GLOBAL");
static readonly GlobalKeyword KeywordFlowSlope = GlobalKeyword.Create("FLOW_SLOPE");
/// <summary>
/// The one place that sets the flow uniforms and keywords: every shader flow term derives from FlowSettings and a
/// scroll (FlowField.Scroll), so no presenter keeps its own flow clock.
/// The time the flow runs on: the zone's own (so drift stops with the sim and follows sim speed), or on menus,
/// where no zone exists, the presentation clock.
/// </summary>
public static void LowerFlow(FlowSettings flow, float scroll)
public static double FlowClock => ActionGameManager.Instance?.Zone?.ExactTime ?? Time.timeSinceLevelLoad;
/// <summary>
/// The one place that sets the flow uniforms and keywords: every shader flow term derives from FlowSettings and
/// the flow clock through FlowField, so no presenter keeps its own flow clock.
/// </summary>
public static void LowerFlow(FlowSettings flow, double flowClock)
{
Shader.SetGlobalFloat("_FlowScale", flow.GlobalScale);
Shader.SetGlobalFloat("_FlowAmplitude", flow.GlobalAmplitude);
Shader.SetGlobalFloat("_FlowSlopeAmplitude", flow.SlopeAmplitude);
Shader.SetGlobalFloat("_FlowSwirlAmplitude", flow.SwirlAmplitude);
Shader.SetGlobalFloat("_FlowScroll", scroll);
var scroll = FlowField.Scroll(flow, flowClock);
Shader.SetGlobalVector("_FlowScroll", new Vector4(scroll.x, scroll.y));
Shader.SetGlobalFloat("_FlowPeriod", flow.Period);
Shader.SetGlobalFloat("_FlowCycle", FlowField.Cycle(flowClock, flow.Period));
Shader.SetKeyword(KeywordFlowGlobal, flow.GlobalAmplitude != 0);
Shader.SetKeyword(KeywordFlowSlope, flow.SlopeAmplitude != 0 || flow.SwirlAmplitude != 0);

View file

@ -9,6 +9,7 @@ StructuredBuffer<float3> positions;
StructuredBuffer<float2> gradients;
RWStructuredBuffer<float3> globalOut;
RWStructuredBuffer<float3> slopeOut;
RWStructuredBuffer<float3> gatedOut; // flow(): the keyword-gated function the nebula and stardust call
uint count;
[numthreads(64, 1, 1)]
@ -17,4 +18,5 @@ void Evaluate(uint3 id : SV_DispatchThreadID)
if (id.x >= count) return;
globalOut[id.x] = globalFlow(positions[id.x]);
slopeOut[id.x] = slopeFlow(gradients[id.x]);
gatedOut[id.x] = flow(positions[id.x]);
}

View file

@ -25,7 +25,7 @@ Texture2D HueTexture;
SamplerState samplerHueTexture;
float period;
float time;
float phase; // the cycle phase from zone time (FlowField.Cycle), in [0, 1)
float spacing;
float ceilingHeight;
float floorHeight;
@ -125,7 +125,7 @@ void UpdateParticles(uint3 id : SV_DispatchThreadID, int3 groupID : SV_GroupThre
uint xp = x + xt + offset;
uint yp = y + yt + offset;
const float lifetime = frac(time / period + RandomFirst(xp, yp));
const float lifetime = frac(phase + RandomFirst(xp, yp));
particles[id.x].position = float3(
x * spacing + xt * spacing + perlinNoise1D(RandomValue()*1024)*spacing, 0,

View file

@ -101,7 +101,8 @@ public class Stardust : MonoBehaviour
//Bind resources to compute shader
ParticleCalculation.SetBuffer(_updateParticlesKernel, "particles", _particlesBuffer);
ParticleCalculation.SetFloat("time", Time.time);
// 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);

View file

@ -33,7 +33,7 @@ uniform half _NebulaFillDensity,
_NebulaNoiseSlopeExponent,
_FlowScale,
_FlowAmplitude,
_FlowScroll,
_FlowCycle,
_FlowPeriod,
_FlowSlopeAmplitude,
_FlowSwirlAmplitude,
@ -42,6 +42,7 @@ uniform half _NebulaFillDensity,
_DynamicLodHigh,
_DynamicIntensity,
_DynamicSkyBoost;
float2 _FlowScroll;
inline float tri(in float x){return abs(frac(x)-.5);}
inline float3 tri3(in float3 p){return float3( tri(p.z+tri(p.y*1.)), tri(p.z+tri(p.x*1.)), tri(p.y+tri(p.x*1.)));}
@ -66,10 +67,11 @@ float triNoise3d(in float3 p)
return rz;
}
float3 Tri3D( float3 P )
// The two octaves' positions arrive already scrolled (FlowField.Scroll folds each octave's phase in double).
float3 Tri3D( float3 P, float3 PGolden )
{
float3 t1 = normalize(tri3(P));
float3 t2 = normalize(tri3(P * 1.61803398875));
float3 t2 = normalize(tri3(PGolden));
return cross(t1,t2);
}
@ -129,7 +131,8 @@ float3 gravNormal (float2 grad)
float3 globalFlow(float3 pos)
{
float3 flowSample = Tri3D( pos / _FlowScale - float3(0,_FlowScroll,0) ) * _FlowAmplitude;
const float3 field = pos / _FlowScale;
float3 flowSample = Tri3D( field - float3(0,_FlowScroll.x,0), field * 1.61803398875 - float3(0,_FlowScroll.y,0) ) * _FlowAmplitude;
flowSample.y *= .5;
return flowSample;
}
@ -198,13 +201,13 @@ float density(float3 pos)
#endif
#endif
const float lerp1 = frac(_Time.y / _FlowPeriod);
const float lerp2 = frac(_Time.y / _FlowPeriod + .5);
const float lerp1 = frac(_FlowCycle);
const float lerp2 = frac(_FlowCycle + .5);
const float noise1 = pow(triNoise3d((pos+fl.xyz * (lerp1 - .5) * _FlowPeriod) / _NebulaNoiseScale),exponent) * tri2(lerp1);
const float noise2 = pow(triNoise3d((pos+fl.xyz * (lerp2 - .5) * _FlowPeriod) / _NebulaNoiseScale),exponent) * tri2(lerp2);
pos.y += (noise1 + noise2) * heightFade * _NebulaNoiseAmplitude;
const float lerp3 = frac(_Time.y / _FlowPeriod * 2 + .25);
const float lerp4 = frac(_Time.y / _FlowPeriod * 2 + .75);
const float lerp3 = frac(_FlowCycle * 2 + .25);
const float lerp4 = frac(_FlowCycle * 2 + .75);
const float noise3 = pow(triNoise3d((pos+fl.xyz * (lerp3 - .5) * _FlowPeriod / 2) / _NebulaNoiseScale * 8), exponent) * tri2(lerp3);
const float noise4 = pow(triNoise3d((pos+fl.xyz * (lerp4 - .5) * _FlowPeriod / 2) / _NebulaNoiseScale * 8), exponent) * tri2(lerp4);
pos.y -= (noise3 + noise4) * heightFade * _NebulaNoiseAmplitude / 2;

View file

@ -74,8 +74,8 @@ public sealed class FlowFieldTests : IDisposable
var shift = flow.GlobalScale * flow.GlobalScrollSpeed * (float) t;
foreach (var p in Probes)
AssertClose(start.Flow(new float3(p.x, p.y - shift, p.z)), later.Flow(p));
Assert.Equal(flow.GlobalScrollSpeed * t, later.FlowScroll, 6);
Assert.Equal(0f, start.FlowScroll);
Assert.Equal(flow.GlobalScrollSpeed * t, later.FlowScroll.x, 6);
Assert.Equal(float2(0f, 0f), start.FlowScroll);
}
[Fact]
@ -88,7 +88,8 @@ public sealed class FlowFieldTests : IDisposable
Assert.Equal(1.0, fine.Time, 4);
Assert.Equal(1.0, coarse.Time, 4);
foreach (var p in Probes) AssertClose(fine.Flow(p), coarse.Flow(p));
Assert.Equal(fine.FlowScroll, coarse.FlowScroll, 5);
Assert.Equal(fine.FlowScroll.x, coarse.FlowScroll.x, 5);
Assert.Equal(fine.FlowScroll.y, coarse.FlowScroll.y, 5);
// The flow moved: a scroll of 0.025 per second is a different field from the start, somewhere.
var start = ZoneWith(Flow());
Assert.Contains(Probes, p => length(start.Flow(p) - fine.Flow(p)) > .05f);
@ -149,4 +150,62 @@ public sealed class FlowFieldTests : IDisposable
// The flow is live (the origin is a legitimate zero of the field, so it is not every probe).
Assert.Contains(Probes, p => length(one.Flow(p)) > 1f);
}
[Fact]
public void ZoneFlowIsTheLawAtTheScrollTheShaderGets()
{
// Zone.Flow and the lowered scroll are one zone-time value: the flow at p is the law evaluated at the zone's
// own FlowScroll, after any run of steps (a flow that ran ahead of or behind the shader's scroll differs here).
var zone = ZoneWith(Flow(), 3.25);
for (var step = 0; step < 90; step++)
{
zone.Update(1f / 60f);
foreach (var p in Probes) AssertClose(FlowField.Global(Flow(), p, zone.FlowScroll), zone.Flow(p), 1e-4f);
}
}
[Fact]
public void PausedSimFreezesTheFlowPhases()
{
// Presenters take their cycle phase from zone time: a zone that does not advance does not move them, and a
// zone that does moves them by dt / period (mod 1).
const float period = 8f;
var zone = ZoneWith(Flow(), 5.0);
var before = zone.CyclePhase(period);
var scroll = zone.FlowScroll;
zone.Update(0f);
Assert.Equal(before, zone.CyclePhase(period));
Assert.Equal(scroll, zone.FlowScroll);
zone.Update(2f);
Assert.Equal(frac(before + 2f / period), zone.CyclePhase(period), 5);
Assert.InRange(zone.CyclePhase(period), 0f, .9999999f);
}
[Theory]
[InlineData(1)]
[InlineData(10)]
public void ScrollKeepsAdvancingAtDaysOfZoneTime(int days)
{
// Zone time persists and only grows. A float of speed x time stops resolving a 1/60 s step after days; the
// phases the shader gets stay in [0, 1) and advance by speed x dt on every step.
const double dt = 1.0 / 60.0;
var flow = Flow();
var zone = ZoneWith(flow, days * 86400.0 + .1);
var previous = zone.FlowScroll;
for (var step = 0; step < 30; step++)
{
zone.Update((float) dt);
var now = zone.FlowScroll;
Assert.InRange(now.x, 0f, .9999999f);
Assert.InRange(now.y, 0f, .9999999f);
var dx = now.x - previous.x;
var dy = now.y - previous.y;
if (dx < -.5f) dx += 1f;
if (dy < -.5f) dy += 1f;
var expected = flow.GlobalScrollSpeed * dt;
Assert.InRange((double) dx, expected - 5e-7, expected + 5e-7);
Assert.InRange((double) dy, expected * 1.61803398875 - 5e-7, expected * 1.61803398875 + 5e-7);
previous = now;
}
}
}

View file

@ -28,21 +28,24 @@ UNITY_EXE = r"C:\Program Files\Unity\Hub\Editor\6000.3.24f1\Editor\Unity.exe"
CGINC = "Assets/Shaders/Volumetric.cginc"
VOLUME_SAMPLING = "Assets/Scripts/Zone Display/VolumeSampling.cs"
FLOW_FIELD = "Assets/Scripts/ServerShared/FlowField.cs"
ZONE = "Assets/Scripts/ServerShared/Zone.cs"
SETTINGS_ASSET = "Assets/Resources/Settings.asset"
# (label, file, before, after, the failing check the probe must name)
MUTANTS = [
("hlsl-y-halving", CGINC, "flowSample.y *= .5;", "flowSample.y *= .6;", "global"),
("hlsl-scroll-sign", CGINC, "pos / _FlowScale - float3(0,_FlowScroll,0)", "pos / _FlowScale + float3(0,_FlowScroll,0)", "global"),
("hlsl-y-factor-501", CGINC, "flowSample.y *= .5;", "flowSample.y *= .501;", "global"),
("hlsl-amplitude-1.0005", CGINC, "- float3(0,_FlowScroll.y,0) ) * _FlowAmplitude;", "- float3(0,_FlowScroll.y,0) ) * _FlowAmplitude * 1.0005;", "global"),
("hlsl-scroll-sign", CGINC, "field - float3(0,_FlowScroll.x,0)", "field + float3(0,_FlowScroll.x,0)", "global"),
("hlsl-scroll2-dropped", CGINC, "field * 1.61803398875 - float3(0,_FlowScroll.y,0)", "field * 1.61803398875", "global"),
("hlsl-swirl-flipped", CGINC, "float3(ngrad.y, 0, -ngrad.x)", "float3(-ngrad.y, 0, ngrad.x)", "slope"),
("hlsl-swirl-1.0005", CGINC, "float3(ngrad.y, 0, -ngrad.x) * _FlowSwirlAmplitude", "float3(ngrad.y, 0, -ngrad.x) * _FlowSwirlAmplitude * 1.0005", "slope"),
("hlsl-zero-gradient-guard", CGINC, " if (dot(grad, grad) == 0) return 0;\n", "", "slope"),
("lower-scroll-doubled", VOLUME_SAMPLING, 'Shader.SetGlobalFloat("_FlowScroll", scroll);', 'Shader.SetGlobalFloat("_FlowScroll", scroll * 2f);', "lowered-scroll"),
("lower-scroll-doubled", VOLUME_SAMPLING, "new Vector4(scroll.x, scroll.y)", "new Vector4(scroll.x * 2f, scroll.y)", "lowered-scroll"),
("lower-scale-halved", VOLUME_SAMPLING, 'Shader.SetGlobalFloat("_FlowScale", flow.GlobalScale);', 'Shader.SetGlobalFloat("_FlowScale", flow.GlobalScale * .5f);', "lowered-settings"),
("lower-frame-clock", VOLUME_SAMPLING, 'Shader.SetGlobalFloat("_FlowScroll", scroll);', 'Shader.SetGlobalFloat("_FlowScroll", scroll + Time.realtimeSinceStartup);', "lowered-scroll"),
("csharp-scale-doubled", FLOW_FIELD, "position / settings.GlobalScale", "position / (settings.GlobalScale * 2f)", "global"),
("csharp-scroll-offset", FLOW_FIELD, "(float) (settings.GlobalScrollSpeed * zoneTime)", "(float) (settings.GlobalScrollSpeed * (zoneTime + 1.0))", "zone-scroll"),
("zone-scroll-scaled", ZONE, "public float FlowScroll => FlowField.Scroll(Settings.Flow, _time);", "public float FlowScroll => FlowField.Scroll(Settings.Flow, _time * 2);", "zone-scroll"),
("lower-frame-clock", VOLUME_SAMPLING, "FlowField.Scroll(flow, flowClock)", "FlowField.Scroll(flow, flowClock + Time.realtimeSinceStartup)", "lowered-scroll"),
("lower-cycle-frame-clock", VOLUME_SAMPLING, "FlowField.Cycle(flowClock, flow.Period)", "FlowField.Cycle(Time.realtimeSinceStartup, flow.Period)", "lowered-cycle"),
("lower-keywords-lookup-only", VOLUME_SAMPLING, 'GlobalKeyword.Create("FLOW_GLOBAL")', 'new GlobalKeyword("FLOW_GLOBAL")', "flow-keywords"),
("csharp-scale-doubled", FLOW_FIELD, "var field = position / settings.GlobalScale;", "var field = position / (settings.GlobalScale * 2f);", "global"),
("asset-amplitude", SETTINGS_ASSET, " GlobalAmplitude: 15\n", " GlobalAmplitude: 14\n", "authored-flow"),
]